3,275 lines118 KBC
1/*
2ui.c: the screens.
3
4 home Homebrew, Themes and Avatars as cards, styled as ps3.world's home page
5 picker a section's categories: Homebrew's (Games, Ports, Tools...) as the site's
6 cards; Themes' and Avatars' (All, then each category or region) as a grid
7 list one list: icon/title/author/version/size per row; Triangle searches
8 (the system keyboard), L2/R2 jump a letter; L1/R1 change section.
9 An avatar list's rows are packs: Cross opens one as a list (a folder)
10 detail the preview (a theme's 480x270, an avatar's 240x240; themes' rows show it
11 small), the description, where it'll be saved, Download (X) with progress,
12 speed and Cancel (O); the result, or why it failed
13 profile which user an avatar is for, when the PS3 has more than one
14
15Lists are never loaded whole: the index says how many items a list has and
16which page file holds each 100, and a page is fetched (or read from the HDD
17cache) when the list reaches it, a few kept in memory. Search reads the list's
18search.txt (one line an item) instead of every page.
19
20The index on the HDD opens at once; the server's is fetched in the background
21and replaces it when it's newer. Cross confirms and Circle goes back, as in
22Resonant (swapped on consoles that confirm with Circle).
23*/
24#include <stdio.h>
25#include <stdlib.h>
26#include <string.h>
27#include <time.h>
28#include "plat.h"
29#include "gfx.h"
30#include "font.h"
31#include "input.h"
32#include "jobs.h"
33#include "catalog.h"
34#include "download.h"
35#include "store.h"
36#include "version.h"
37#include "ui.h"
38#include "lang.h"
39#include "i18n.h"
40#include "thirdparty/cJSON.h"
41
42#define C_BG 0xff05070d
43#define C_TEXT 0xffffffff
44#define C_DIM 0xff9aa4b8
45#define C_ACCENT 0xff2b8fff
46#define C_OK 0xff4cd08a
47#define C_ERR 0xffff7a7a
48#define C_WARN 0xffffc861
49#define C_PANEL 0x8c0a0e18
50#define C_ROW_ON 0x552b8fff
51#define C_CYAN 0xff4ad9ff
52#define C_TEXT2 0xffa2acc0
53#define C_BORDER 0xff1f2533
54
55#define LIST_X 60
56#define LIST_Y 150
57#define LIST_W (SCREEN_W - 2 * LIST_X)
58#define ROW_H 68
59#define ROWS 7
60#define ICON_LIST 56
61#define ICON_BIG 200
62#define GLYPH 28
63#define MAX_RESULTS 2000
64#define CARD_Y 104 // the home and picker cards fill CARD_Y..CARD_Y + CARD_H
65#define CARD_H 528
66#define CARD_GAP 16
67#define TILE_H 112 // the picker's small cards (themes, avatars)
68#define TILE_COLS 3
69#define FAV_H 104 // the home screen's Favourites and Recently added cards, under the sections'
70#define HOME_H (CARD_H - FAV_H - CARD_GAP) // the sections' cards
71#define HOME_FAVS NUM_TYPES // home_sel: the Favourites card
72#define HOME_RECENT (NUM_TYPES + 1) // and the Recently added one
73#define HOME_RW 504 // the right-hand column: Themes, Avatars, Recently added
74#define CAT_W ((LIST_W - 2 * CARD_GAP) / 3) // a Homebrew category card (and its art), three a row
75#define LG_COLS 4 // the language screen: a card per language, LG_COLS a row
76#define LG_W ((LIST_W - (LG_COLS - 1) * CARD_GAP) / LG_COLS)
77#define LG_H 72
78#define LG_TOP 104 // the PS3's-language card, the width of the grid
79#define LG_GRID (LG_TOP + 60 + 22)
80#define LG_ROW(r) (LG_GRID + (r) * (LG_H + 12))
81#define LG_SOUND (LG_ROW((LANG_COUNT + LG_COLS - 1) / LG_COLS) + 10)
82#define QR_PX 132 // the contact QR code: 33 modules, 4 px each at 720p, 6 at 1080p (sharp)
83
84enum { SCR_START, SCR_HOME, SCR_LIST, SCR_DETAIL, SCR_PROFILE, SCR_DOWNLOADS, SCR_LANG, SCR_CREDITS, SCR_NEWS };
85static int news_due; // this version's What's new is shown when the catalogue's in (once: USRDIR/seen.txt)
86
87static int screen = SCR_START;
88static int dirty = 1;
89static uint64_t t_now;
90static image_t *backdrop, *logo, *qr_contact, *flag_img[LANG_COUNT];
91static image_t *logo_big; // the intro's (freed when it's over)
92#define INTRO_US 2100000 // the whole of it
93#define INTRO_FADE_US 1700000 // where it starts fading into the app
94#define INTRO_LOGO_W 560
95#define INTRO_LOGO_H 79 // (logo.png is 1018x143)
96#define INTRO_Y 300 // the logo's middle
97
98static image_t *name_img[LANG_COUNT][2]; // a CJK language's name as a picture: white, and cyan for the one in use (NULL: text)
99static int name_w[LANG_COUNT]; // (qr: ps3.world/contact, made by tools/qr.mjs)
100
101static const char *section_names[NUM_TYPES] = {"Homebrew", "Themes", "Avatars"};
102
103/*
104==================
105Helpers
106==================
107*/
108// data/names/<code>.png (and -on): drawn at twice the size by tools/make-names.mjs, so loaded at half
109// what its PNG header says; NULL when there's none (most languages are drawn as text)
110static image_t *Load_Name(const char *name, int *w)
111{
112 char path[512];
113 size_t len;
114 snprintf(path, sizeof(path), "%s/names/%s", plat_data_dir(), name);
115 unsigned char *data = plat_read_file(path, &len);
116 if (!data) return NULL;
117 image_t *img = NULL;
118 if (len > 24 && !memcmp(data + 12, "IHDR", 4)) {
119 int pw = data[16] << 24 | data[17] << 16 | data[18] << 8 | data[19];
120 int ph = data[20] << 24 | data[21] << 16 | data[22] << 8 | data[23];
121 if (pw > 1 && pw < 2048 && ph > 1 && ph < 256) {
122 *w = pw / 2;
123 img = image_decode_screen(data, (int)len, pw / 2, ph / 2);
124 }
125 }
126 free(data);
127 return img;
128}
129
130static image_t *Load_Image(const char *name, int w, int h)
131{
132 char path[512];
133 size_t len;
134 snprintf(path, sizeof(path), "%s/%s", plat_data_dir(), name);
135 unsigned char *data = plat_read_file(path, &len);
136 if (!data) { plat_log("ui: %s missing", path); return NULL; }
137 image_t *img = image_decode_screen(data, (int)len, w, h);
138 free(data);
139 return img;
140}
141
142static void Fmt_Size(long long b, char *out, int size)
143{
144 if (b >= 1024LL * 1024 * 1024) snprintf(out, size, "%.2f GB", b / 1073741824.0);
145 else if (b >= 1024 * 1024) snprintf(out, size, "%.1f MB", b / 1048576.0);
146 else if (b >= 1024) snprintf(out, size, "%lld KB", b / 1024);
147 else snprintf(out, size, "%lld B", b);
148}
149
150static void Fmt_Count(int n, char *out, int size)
151{
152 if (n >= 1000) snprintf(out, size, "%d,%03d", n / 1000, n % 1000);
153 else snprintf(out, size, "%d", n);
154}
155
156static char Lower(char c) { return c >= 'A' && c <= 'Z' ? c + 32 : c; }
157
158// needle in the first len bytes of hay (any case)
159static int Contains(const char *hay, int len, const char *needle)
160{
161 if (!*needle) return 1;
162 for (int i = 0; i < len; i++) {
163 int k = 0;
164 while (needle[k] && i + k < len && Lower(hay[i + k]) == Lower(needle[k])) k++;
165 if (!needle[k]) return 1;
166 }
167 return 0;
168}
169
170// the letter a title is filed under (generator/make-catalog.mjs letterOf)
171static char Letter_Of(const char *title)
172{
173 for (; *title; title++) {
174 char c = *title;
175 if (c >= 'a' && c <= 'z') return c - 32;
176 if (c >= 'A' && c <= 'Z') return c;
177 if ((c >= '0' && c <= '9') || (unsigned char)c >= 0x80) return '#';
178 }
179 return '#';
180}
181
182// word-wraps s into lines of at most maxw; returns the number of lines
183static int Wrap(int font, int px, const char *s, int maxw, char (*lines)[256], int maxlines)
184{
185 int n = 0;
186 while (*s && n < maxlines) {
187 char line[256];
188 int len = 0, last_space = -1, cl = 1;
189 while (s[len] && s[len] != '\n') {
190 // a whole UTF-8 character at a time: a line never ends inside one
191 for (cl = 1; (s[len + cl] & 0xc0) == 0x80; cl++);
192 if (len + cl > 255) break;
193 memcpy(line, s, len + cl);
194 line[len + cl] = 0;
195 if (font_width(font, px, line) > maxw) break;
196 if (s[len] == ' ') last_space = len;
197 len += cl;
198 }
199 int cut = len;
200 if (s[len] && s[len] != '\n' && last_space > 0) cut = last_space;
201 if (cut == 0) cut = len ? len : s[0] ? cl : 1;
202 snprintf(lines[n++], 256, "%.*s", cut, s);
203 s += cut;
204 while (*s == ' ' || *s == '\n') s++;
205 }
206 if (*s && n) { // more than fits: the last line ends in ...
207 size_t l = strlen(lines[n - 1]);
208 if (l > 3) {
209 l -= 3;
210 while (l && (lines[n - 1][l] & 0xc0) == 0x80) l--; // (not inside a character)
211 strcpy(lines[n - 1] + l, "...");
212 }
213 }
214 return n;
215}
216
217// controller glyphs (Kenney's Input Prompts, CC0)
218enum { G_CROSS, G_CIRCLE, G_SQUARE, G_TRIANGLE, G_L1, G_R1, G_L2, G_R2, G_DPAD_UD, G_SELECT, G_START, NUM_GLYPHS };
219static const char *glyph_files[NUM_GLYPHS] = {"cross", "circle", "square", "triangle", "l1", "r1", "l2", "r2", "dpad_ud", "select", "start"};
220static image_t *glyphs[NUM_GLYPHS];
221
222static void Glyph(int g, int x, int y)
223{
224 // on a console that confirms with O, the button that does "X" things is O (input.c swaps them)
225 if (plat_circle_confirms() && (g == G_CROSS || g == G_CIRCLE)) g = g == G_CROSS ? G_CIRCLE : G_CROSS;
226 if (glyphs[g]) gfx_image(glyphs[g], x, y, GLYPH, GLYPH, 0, 255);
227 else gfx_circle(x + GLYPH / 2, y + GLYPH / 2, GLYPH / 2 - 2, 0xff55585e);
228}
229
230// "<glyph> label" along the bottom; returns the x after it
231static int Hint(int x, int g, const char *label)
232{
233 int y = SCREEN_H - 58;
234 Glyph(g, x, y);
235 return x + GLYPH + 8 + font_draw(FONT_MEDIUM, 19, x + GLYPH + 8, y + 3, label, C_DIM, 0) + 28;
236}
237
238static int Hint2(int x, int g1, int g2, const char *label)
239{
240 Glyph(g1, x, SCREEN_H - 58);
241 return Hint(x + GLYPH + 4, g2, label);
242}
243
244// L1, a row of pills (the one in use filled), R1: what L1/R1 move through. When they don't all
245// fit before maxx, the ones around the one in use. Returns the x after R1.
246#define TAB_H 34
247static int Tabs(int x, int y, int maxx, const char *const *labels, int n, int sel)
248{
249 int w[32];
250 if (n > 32) n = 32;
251 if (n <= 0) return x;
252 if (sel < 0) sel = 0;
253 if (sel >= n) sel = n - 1;
254 for (int i = 0; i < n; i++) w[i] = font_width(FONT_MEDIUM, 20, labels[i]) + 30;
255 int room = maxx - x - 2 * (GLYPH + 12), a = sel, b = sel, used = w[sel];
256 for (int grew = 1; grew; ) {
257 grew = 0;
258 if (b + 1 < n && used + 6 + w[b + 1] <= room) { used += 6 + w[++b]; grew = 1; }
259 if (a > 0 && used + 6 + w[a - 1] <= room) { used += 6 + w[--a]; grew = 1; }
260 }
261 Glyph(G_L1, x, y + (TAB_H - GLYPH) / 2);
262 x += GLYPH + 12;
263 for (int i = a; i <= b; i++) {
264 if (i == sel) gfx_round_rect(x, y, w[i], TAB_H, TAB_H / 2, C_ROW_ON);
265 font_draw(FONT_MEDIUM, 20, x + 15, y + (TAB_H - 20) / 2, labels[i], i == sel ? C_TEXT : C_DIM, w[i] - 30);
266 x += w[i] + 6;
267 }
268 Glyph(G_R1, x + 6, y + (TAB_H - GLYPH) / 2);
269 return x + 6 + GLYPH;
270}
271
272
273/*
274==================
275Cards, as ps3.world's .explore-card: the art faded in from the right, a cyan
276kicker, the title, a line or two, and chips (data/cards: tools/make-art.mjs)
277==================
278*/
279typedef struct {
280 const char *list, *art, *kicker, *title, *blurb;
281} card_text_t;
282
283static const card_text_t home_cards[NUM_TYPES] = {
284 {NULL, "home-homebrew", "Apps · Games · Ports", "Homebrew", "Homebrew tools and games, PC ports and media players for HEN & CFW."},
285 {NULL, "home-themes", "Customise your XMB", "Themes", "Custom themes from the full theme archive, by category."},
286 {NULL, "home-avatars", "Your profile picture", "Avatars", "XMB avatars from every region, saved to your PS3 user."},
287};
288static const card_text_t cat_cards[] = {
289 {"Games", "cat-games", "Play", "Homebrew Games", "Grid Siege, Super Mario War, PS3Sweeper+ and more."},
290 {"Ports", "cat-ports", "PC & retro", "Ports", "Undertale, Nazi Zombies: Portable, Mario Kart 64 and more. No game files needed."},
291 {"Edits", "cat-edits", "PS1 & PS2 mods", "Edits", "Modded PS1 and PS2 classics: Budokai Tenkaichi 3, WWE 2K24, WCW Rebooked and more."},
292 {"Tools", "cat-tools", "Essentials", "Homebrew Tools", "Apollo, IRISMAN, webMAN MOD, PKGi, multiMAN and more."},
293 {"Media", "cat-media", "Watch & listen", "Media Players", "Movian, Yo! Player, Jellyfin, Spotify and Resonant for Plex."},
294 {"Misc", "cat-misc", "Prototypes & demos", "Miscellaneous", "Unreleased prototypes, demos and more."},
295};
296#define NUM_CAT_CARDS ((int)(sizeof(cat_cards) / sizeof(cat_cards[0])))
297
298static image_t *card_bg, *home_art[NUM_TYPES], *cat_art[NUM_CAT_CARDS];
299
300static const card_text_t *Cat_Card(const char *list, image_t **art)
301{
302 for (int i = 0; i < NUM_CAT_CARDS; i++)
303 if (!strcmp(cat_cards[i].list, list)) { *art = cat_art[i]; return &cat_cards[i]; }
304 *art = NULL;
305 return NULL;
306}
307
308// the card's frame: a blue ring when it's selected, the site's 1px border when not, and its gradient
309static void Card_Frame(int x, int y, int w, int h, int on)
310{
311 if (on) gfx_round_rect(x - 3, y - 3, w + 6, h + 6, 21, C_ACCENT);
312 else gfx_round_rect(x - 1, y - 1, w + 2, h + 2, 19, C_BORDER);
313 if (card_bg) gfx_image_flip(card_bg, x, y, w, h, 18, 255); // (its glow on the side the art's on)
314 else gfx_round_rect(x, y, w, h, 18, 0xff0e131f);
315}
316
317#define CHIP_H 36
318
319static int Chip(int x, int y, const char *text)
320{
321 int w = font_width(FONT_MEDIUM, 18, text) + 32;
322 gfx_round_rect(x, y, w, CHIP_H, CHIP_H / 2, 0x1fffffff);
323 gfx_round_rect(x + 1, y + 1, w - 2, CHIP_H - 2, CHIP_H / 2 - 1, 0xbf05070d);
324 font_draw(FONT_MEDIUM, 18, x + 16, y + (CHIP_H - 18) / 2, text, C_TEXT2, 0); // (font_draw centres capitals in px)
325 return w;
326}
327
328// font_draw centres the capitals in a box px high (font.c Ascent), Em 0.8 px and caps 0.7 Em:
329// where the letters you see start and end below its y
330static int Cap_Top(int px) { return px * 22 / 100; }
331static int Cap_Bottom(int px) { return px * 78 / 100; }
332
333// (art_w: the art's width, at the card's right, when the card's wider than it; 0: the card's)
334static void Card(int x, int y, int w, int h, int on, const image_t *art, int art_w, const char *kicker, const char *title,
335 int title_px, const char *blurb, const char **chips, int num_chips)
336{
337 Card_Frame(x, y, w, h, on);
338 if (art_w <= 0 || art_w > w) art_w = w;
339 if (art) gfx_image_flip(art, x + w - art_w, y, art_w, h, 18, on ? 255 : 160); // (right to left: the art's on the left) // the site's 0.8 on hover, 0.5 off
340 // spaced by the letters, not their boxes: 16 px from the kicker to the title, 20 to the text
341 int tx = x + 28, ty = y + 24, chip_y = y + h - 26 - CHIP_H;
342 if (kicker) {
343 font_draw(FONT_BOLD, 20, tx, ty, tr(kicker), C_CYAN, w - 56);
344 ty += Cap_Bottom(20) + 16 - Cap_Top(title_px);
345 }
346 font_draw(FONT_BOLD, title_px, tx, ty, tr(title), C_TEXT, w - 56);
347 ty += Cap_Bottom(title_px) + 20 - Cap_Top(21);
348 char lines[4][256];
349 int max = (chip_y - 8 - ty) / 32;
350 int bw = w * 2 / 3 < 340 ? w * 2 / 3 : 340; // (the site's max-width: 34ch, about; the art shows to its right)
351 int n = blurb && max > 0 ? Wrap(FONT_REGULAR, 21, tr(blurb), bw, lines, max > 4 ? 4 : max) : 0;
352 for (int i = 0; i < n; i++)
353 font_draw(FONT_REGULAR, 21, tx, ty + i * 32, lines[i], C_TEXT2, 0);
354 // the chips that fit on one line
355 int cx = tx;
356 for (int i = 0; i < num_chips; i++) {
357 if (cx + font_width(FONT_MEDIUM, 18, tr(chips[i])) + 32 > x + w - 28) break;
358 cx += Chip(cx, chip_y, tr(chips[i])) + 8;
359 }
360}
361
362// a small card: a category of themes or avatars, and how many
363static void Tile(int x, int y, int w, int h, int on, const char *title, const char *count)
364{
365 Card_Frame(x, y, w, h, on);
366 font_draw(FONT_BOLD, 26, x + 24, y + 22, tr(title), C_TEXT, w - 48);
367 font_draw(FONT_MEDIUM, 19, x + 24, y + h - 44, count, on ? C_CYAN : C_TEXT2, w - 48);
368}
369
370static const char *Error_Text(int kind, const char *detail_msg)
371{
372 static char buf[300];
373 switch (kind) {
374 case ERR_NETWORK:
375 return tr("No network connection. Check Settings > Network Settings on the PS3, then try again.");
376 case ERR_TLS:
377 return tr("The secure connection to dl.ps3.world failed. Check the PS3's date and time (Settings > Date and Time Settings), then try again.");
378 case ERR_NOT_FOUND:
379 return tr("Not found on the server (404). It may have moved: try again in a moment.");
380 case ERR_DISK_FULL:
381 snprintf(buf, sizeof(buf), tr("Not enough free space. %s"), detail_msg);
382 return buf;
383 }
384 snprintf(buf, sizeof(buf), "%s", detail_msg);
385 return buf;
386}
387
388/*
389==================
390Pages: fetched (or read from the HDD) as the lists reach them, a few kept
391==================
392*/
393#define PAGE_SLOTS 16
394#define RETRY_US 5000000
395
396typedef struct {
397 unsigned char sha[32]; // which file (the index's sha256 names it)
398 int search; // a search.txt rather than a page
399 catalog_t *page;
400 struct search_s *lines;
401 job_t *job;
402 int failed, err_kind;
403 char err[160];
404 uint64_t used, failed_at;
405} file_slot_t;
406
407typedef struct search_s {
408 char *text;
409 const char **line; // each item's line ("title\tauthor\tgroup")
410 int *len;
411 int count;
412} search_t;
413
414static file_slot_t files[PAGE_SLOTS];
415
416typedef struct {
417 cat_file_t f;
418 int search;
419} file_req_t;
420
421static void Search_Free(search_t *s)
422{
423 if (!s) return;
424 free(s->text);
425 free(s->line);
426 free(s->len);
427 free(s);
428}
429
430static void File_Run(job_t *j)
431{
432 file_req_t *r = j->arg;
433 size_t len;
434 char *body = store_get_file(&r->f, &len, &j->cancel, &j->ival, j->err, sizeof(j->err));
435 if (!body) return;
436 if (!r->search) {
437 j->result = catalog_parse_page(body, len, j->err, sizeof(j->err));
438 free(body);
439 return;
440 }
441 search_t *s = calloc(1, sizeof(*s));
442 int n = 0;
443 for (size_t i = 0; i < len; i++) if (body[i] == '\n') n++;
444 if (!s || !(s->line = malloc(sizeof(char *) * (n + 1))) || !(s->len = malloc(sizeof(int) * (n + 1)))) {
445 Search_Free(s);
446 free(body);
447 return;
448 }
449 s->text = body;
450 for (char *p = body; *p && s->count <= n; ) {
451 char *e = strchr(p, '\n');
452 s->line[s->count] = p;
453 s->len[s->count] = e ? (int)(e - p) : (int)strlen(p);
454 s->count++;
455 if (!e) break;
456 p = e + 1;
457 }
458 j->result = s;
459}
460
461static void File_Cleanup(job_t *j)
462{
463 file_req_t *r = j->arg;
464 if (r && r->search) Search_Free(j->result);
465 else catalog_free(j->result);
466 free(r);
467}
468
469static void Files_Tick(void)
470{
471 for (int i = 0; i < PAGE_SLOTS; i++) {
472 file_slot_t *s = &files[i];
473 if (!job_done(s->job)) continue;
474 if (s->search) s->lines = s->job->result;
475 else s->page = s->job->result;
476 s->job->result = NULL;
477 s->failed = !s->page && !s->lines;
478 s->err_kind = s->job->ival;
479 snprintf(s->err, sizeof(s->err), "%s", s->job->err);
480 if (s->failed) {
481 s->failed_at = t_now;
482 plat_log("catalog: %s", s->err);
483 }
484 job_release(s->job);
485 s->job = NULL;
486 dirty = 1;
487 }
488}
489
490// the slot for a file, asked for if it isn't there; NULL if every slot is busy
491static file_slot_t *File_Get(const cat_file_t *f, int search)
492{
493 int lru = -1;
494 for (int i = 0; i < PAGE_SLOTS; i++) {
495 file_slot_t *s = &files[i];
496 if ((s->page || s->lines || s->job || s->failed) && s->search == search && !memcmp(s->sha, f->sha256, 32)) {
497 s->used = t_now;
498 if (!s->failed || t_now - s->failed_at < RETRY_US) return s;
499 lru = i; // failed a while ago: try again in the same slot
500 break;
501 }
502 if (!s->job && (lru < 0 || s->used < files[lru].used)) lru = i;
503 }
504 if (lru < 0) return NULL;
505 file_req_t *r = calloc(1, sizeof(*r));
506 job_t *j = r ? job_new(File_Run, File_Cleanup) : NULL;
507 if (!j) { free(r); return NULL; }
508 r->f = *f;
509 r->search = search;
510 j->arg = r;
511 file_slot_t *s = &files[lru];
512 catalog_free(s->page);
513 Search_Free(s->lines);
514 memset(s, 0, sizeof(*s));
515 memcpy(s->sha, f->sha256, 32);
516 s->search = search;
517 s->used = t_now;
518 s->job = j;
519 job_post(j, LANE_API);
520 return s;
521}
522
523/*
524==================
525The index, sections and their lists
526==================
527*/
528static cat_index_t *ix;
529static cat_index_t *pending; // a newer one, used when the detail screen isn't open
530static job_t *fetch_job;
531static char banner[200]; // "Offline: ..." over the list
532static int search_banner; // (it's "Search isn't available")
533static char start_msg[300]; // the start screen's line
534static int start_err;
535
536typedef struct {
537 int sel, top;
538 char query[128];
539 int *results; // positions that match the search (NULL: the whole list)
540 int num_results;
541 int search_wait; // the search.txt is on its way
542 int sort; // SORT_*: which order of the list (when the catalogue has it)
543} view_t;
544
545static view_t *views[NUM_TYPES]; // one per list
546static int cur; // the section shown
547static int cur_list[NUM_TYPES]; // its list, or -1 for its category picker
548static int picker_sel, picker_top; // (picker_top: the first row of tiles shown)
549static int home_sel;
550static int recent_mode; // the list is a Recently added, from the home screen (L1/R1: the sections')
551static cat_list_t folder; // an avatar pack opened from a list, shown as a list (pages NULL: none)
552static view_t folder_view;
553
554static cat_list_t *List(int t, int l) { return ix && l >= 0 && l < ix->num_lists[t] ? &ix->lists[t][l] : NULL; }
555// each section's Recently added list (-1: none): opened from the home screen, not among its categories
556static int Recent_Of(int t)
557{
558 for (int l = 0; ix && l < ix->num_lists[t]; l++)
559 if (!strcmp(ix->lists[t][l].name, "Recently added")) return l;
560 return -1;
561}
562// the categories the picker and the tabs show (the lists but Recently added): how many, and which list each is
563static int Cats(int t) { return ix ? ix->num_lists[t] - (Recent_Of(t) >= 0) : 0; }
564static int Cat_List(int t, int i) { int r = Recent_Of(t); return r >= 0 && i >= r ? i + 1 : i; }
565static int List_Cat(int t, int l) { int r = Recent_Of(t); return r >= 0 && l > r ? l - 1 : l; }
566static int Picking(void) { return ix && Cats(cur) > 1 && cur_list[cur] < 0; }
567// the list picked (in A-Z order)
568static cat_list_t *Base_List(void)
569{
570 if (folder.pages) return &folder;
571 return Picking() ? NULL : List(cur, cur_list[cur] < 0 ? 0 : cur_list[cur]);
572}
573static view_t *Cur_View(void)
574{
575 if (folder.pages) return &folder_view;
576 return Base_List() && views[cur] ? &views[cur][cur_list[cur] < 0 ? 0 : cur_list[cur]] : NULL;
577}
578// the list on screen: the one picked, in the order its view has
579static cat_list_t *Cur_List(void)
580{
581 cat_list_t *l = Base_List();
582 view_t *v = Cur_View();
583 if (l && v && v->sort > 0 && v->sort <= l->num_sorted) return &l->sorted[v->sort - 1];
584 return l;
585}
586
587static void Folder_Close(void)
588{
589 free(folder_view.results);
590 memset(&folder_view, 0, sizeof(folder_view));
591 catalog_free_list(&folder);
592}
593static int View_Count(const view_t *v, const cat_list_t *l) { return v->results ? v->num_results : l->count; }
594static int View_Pos(const view_t *v, int row) { return v->results ? v->results[row] : row; }
595
596// themes and avatars: a grid of their pictures, GRID_ROWS rows of Grid_Cols (homebrew: a list, ROWS rows)
597#define GRID_ROWS 3
598#define GRID_GAP 12 // between its rows
599static int Gridded(int t) { return t == TYPE_THEME || t == TYPE_AVATAR; }
600static int Grid_Cols(int t) { return t == TYPE_THEME ? 5 : 8; }
601static int Grid_W(int t) { return (LIST_W - CARD_GAP * (Grid_Cols(t) - 1)) / Grid_Cols(t); }
602static int Grid_H(int t) { return t == TYPE_THEME ? Grid_W(t) * 9 / 16 : Grid_W(t); } // (a theme's preview is 16:9)
603
604// sel in range, and top where sel shows (a grid's on the first of a row)
605static void Clamp_In(view_t *v, int count, int t)
606{
607 if (v->sel >= count) v->sel = count - 1;
608 if (v->sel < 0) v->sel = 0;
609 if (Gridded(t)) {
610 int cols = Grid_Cols(t), row = v->sel / cols, top = v->top / cols, last = (count - 1) / cols - GRID_ROWS + 1;
611 if (row < top) top = row;
612 if (row >= top + GRID_ROWS) top = row - GRID_ROWS + 1;
613 if (top > last) top = last;
614 v->top = top > 0 ? top * cols : 0;
615 return;
616 }
617 if (v->top > v->sel) v->top = v->sel;
618 if (v->top < v->sel - ROWS + 1) v->top = v->sel - ROWS + 1;
619 if (v->top > count - ROWS) v->top = count - ROWS;
620 if (v->top < 0) v->top = 0;
621}
622static void Clamp(view_t *v, int count) { Clamp_In(v, count, cur); }
623
624// the item at a position of a list: NULL until its page is here (*slot says how it's going)
625static const cat_item_t *Item_At(const cat_list_t *l, int pos, file_slot_t **slot)
626{
627 int p = pos / ix->page_size;
628 *slot = NULL;
629 if (pos < 0 || pos >= l->count || p >= l->num_pages) return NULL;
630 file_slot_t *s = File_Get(&l->pages[p], 0);
631 *slot = s;
632 if (!s || !s->page) return NULL;
633 int k = pos % ix->page_size;
634 return k < s->page->count ? &s->page->items[k] : NULL;
635}
636
637// the item's search line (title, author, group), if the list's search.txt is here
638static const char *Search_Line(const cat_list_t *l, int pos, int *len)
639{
640 for (int i = 0; i < PAGE_SLOTS; i++)
641 if (files[i].lines && files[i].search && !memcmp(files[i].sha, l->search.sha256, 32) && pos < files[i].lines->count) {
642 *len = files[i].lines->len[pos];
643 return files[i].lines->line[pos];
644 }
645 return NULL;
646}
647
648static void Views_Free(void)
649{
650 for (int t = 0; t < NUM_TYPES; t++) {
651 if (views[t] && ix)
652 for (int l = 0; l < ix->num_lists[t]; l++) free(views[t][l].results);
653 free(views[t]);
654 views[t] = NULL;
655 }
656}
657
658static void Index_Use(cat_index_t *n)
659{
660 // where each list was, kept when the lists are the same ones
661 int same = ix != NULL;
662 for (int t = 0; same && t < NUM_TYPES; t++)
663 same = ix->num_lists[t] == n->num_lists[t];
664 view_t *keep[NUM_TYPES] = {0};
665 for (int t = 0; t < NUM_TYPES; t++) {
666 if (same && views[t]) {
667 keep[t] = views[t];
668 for (int l = 0; l < n->num_lists[t]; l++) {
669 free(keep[t][l].results); // searches start again on the new lists
670 keep[t][l].results = NULL;
671 keep[t][l].query[0] = 0;
672 Clamp_In(&keep[t][l], n->lists[t][l].count, t);
673 }
674 views[t] = NULL;
675 }
676 }
677 Views_Free();
678 Folder_Close(); // (its pages may have changed)
679 catalog_free_index(ix);
680 ix = n;
681 for (int t = 0; t < NUM_TYPES; t++) {
682 views[t] = keep[t] ? keep[t] : calloc(ix->num_lists[t] ? ix->num_lists[t] : 1, sizeof(view_t));
683 if (!same) cur_list[t] = Cats(t) > 1 ? -1 : Cat_List(t, 0);
684 }
685 if (!same) {
686 picker_sel = picker_top = 0;
687 recent_mode = 0;
688 }
689 if (screen == SCR_START) screen = news_due ? SCR_NEWS : SCR_HOME;
690 dirty = 1;
691}
692
693static void Fetch_Run(job_t *j)
694{
695 http_res_t res;
696 size_t len = 0;
697 char *body = http_fetch(NULL, CATALOG_URL, NULL, NULL, 30, &j->cancel, &len, &res);
698
699 if (!body || res.status != 200) {
700 j->ival = download_error_kind(res.err, body ? res.status : 0);
701 if (body) snprintf(j->err, sizeof(j->err), tr("The catalogue wasn't found on the server (HTTP %d)"), res.status);
702 else snprintf(j->err, sizeof(j->err), "%s", res.err[0] ? res.err : "The catalogue didn't download");
703 free(body);
704 return;
705 }
706 cat_index_t *n = catalog_parse_index(body, len, j->err, sizeof(j->err));
707 if (n) store_save_index(body, len);
708 j->result = n;
709 free(body);
710}
711
712static void Fetch_Cleanup(job_t *j)
713{
714 catalog_free_index(j->result);
715}
716
717static void Fetch_Start(void)
718{
719 if (fetch_job) return;
720 if (!ix) {
721 snprintf(start_msg, sizeof(start_msg), "%s", tr("Loading the catalogue..."));
722 start_err = 0;
723 }
724 fetch_job = job_new(Fetch_Run, Fetch_Cleanup);
725 if (fetch_job) job_post(fetch_job, LANE_API);
726 dirty = 1;
727}
728
729static void Fetch_Tick(void)
730{
731 if (!job_done(fetch_job))
732 return;
733 cat_index_t *n = fetch_job->result;
734 fetch_job->result = NULL;
735 if (n) {
736 plat_log("catalog: index of %d homebrew (%d lists), %d themes, %d avatars, generated %s",
737 n->count[TYPE_HOMEBREW], n->num_lists[TYPE_HOMEBREW], n->count[TYPE_THEME], n->count[TYPE_AVATAR], n->generated);
738 banner[0] = 0;
739 if (ix && !strcmp(ix->generated, n->generated)) {
740 catalog_free_index(n); // the one we have
741 } else if (screen == SCR_DETAIL || screen == SCR_PROFILE) {
742 catalog_free_index(pending);
743 pending = n;
744 } else
745 Index_Use(n);
746 } else {
747 plat_log("catalog: %s", fetch_job->err);
748 if (ix) {
749 snprintf(banner, sizeof(banner), tr("Offline: showing the catalogue from %.10s"), ix->generated);
750 search_banner = 0;
751 } else {
752 snprintf(start_msg, sizeof(start_msg), "%s", Error_Text(fetch_job->ival, fetch_job->err));
753 start_err = 1;
754 }
755 }
756 job_release(fetch_job);
757 fetch_job = NULL;
758 dirty = 1;
759}
760
761// "Search isn't available" goes with the search it was about (an offline banner stays)
762static void Search_Banner_Clear(void)
763{
764 if (search_banner) banner[0] = search_banner = 0;
765}
766
767// the search, once the list's search.txt is here
768static void Search_Run(view_t *v, const cat_list_t *l)
769{
770 file_slot_t *s = File_Get(&l->search, 1);
771 if (!s || !s->lines) {
772 v->search_wait = !(s && s->failed);
773 if (s && s->failed) {
774 // no search file (offline and never fetched): say so, and match nothing
775 snprintf(banner, sizeof(banner), tr("Search isn't available: %s"), Error_Text(s->err_kind, s->err));
776 search_banner = 1;
777 free(v->results);
778 v->results = malloc(sizeof(int));
779 v->num_results = 0;
780 dirty = 1;
781 }
782 return;
783 }
784 v->search_wait = 0;
785 free(v->results);
786 v->results = malloc(sizeof(int) * MAX_RESULTS);
787 v->num_results = 0;
788 for (int i = 0; v->results && i < s->lines->count && v->num_results < MAX_RESULTS; i++)
789 if (Contains(s->lines->line[i], s->lines->len[i], v->query))
790 v->results[v->num_results++] = i;
791 v->sel = v->top = 0;
792 dirty = 1;
793}
794
795/*
796==================
797Icons and previews: from the HDD cache, or fetched once and cached
798==================
799*/
800#define ICON_SLOTS 96
801#define ICON_INFLIGHT 12
802
803typedef struct {
804 char id[64];
805 int w, h, preview;
806 image_t *img;
807 job_t *job;
808 int failed;
809 uint64_t used, failed_at;
810} icon_slot_t;
811#define ART_RETRY_US 30000000 // art that failed is asked for again after this
812static icon_slot_t icons[ICON_SLOTS];
813static int icons_inflight;
814
815typedef struct {
816 char url[1100];
817 char path[600];
818 int w, h;
819} icon_req_t;
820
821static void Icon_Run(job_t *j)
822{
823 icon_req_t *r = j->arg;
824 size_t len = 0;
825 unsigned char *data = plat_read_file(r->path, &len);
826 if (data) {
827 j->result = image_decode_screen(data, (int)len, r->w, r->h);
828 free(data);
829 if (j->result) return;
830 remove(r->path); // damaged in the cache: fetch it again
831 }
832 http_res_t res;
833 char *body = http_fetch(NULL, r->url, NULL, NULL, 20, &j->cancel, &len, &res);
834 if (body && res.status == 200) {
835 j->result = image_decode_screen(body, (int)len, r->w, r->h);
836 if (j->result) plat_write_file(r->path, body, len); // only what decodes is kept
837 }
838 free(body);
839}
840
841static void Icon_Cleanup(job_t *j)
842{
843 free(j->arg);
844 image_free(j->result);
845}
846
847static void Icons_Tick(void)
848{
849 for (int i = 0; i < ICON_SLOTS; i++) {
850 icon_slot_t *s = &icons[i];
851 if (!job_done(s->job)) continue;
852 s->img = s->job->result;
853 s->job->result = NULL;
854 s->failed = !s->img;
855 s->failed_at = t_now;
856 job_release(s->job);
857 s->job = NULL;
858 icons_inflight--;
859 dirty = 1;
860 }
861}
862
863// an item's icon, or its preview, at w x h: NULL until it's here (*failed: it won't be)
864static image_t *Art_Get(const cat_item_t *it, int preview, int w, int h, int *failed)
865{
866 int lru = -1;
867 const char *rel = preview ? it->preview : it->icon;
868 if (failed) *failed = !rel[0];
869 if (!rel[0]) return NULL;
870 for (int i = 0; i < ICON_SLOTS; i++) {
871 icon_slot_t *s = &icons[i];
872 if (s->w == w && s->h == h && s->preview == preview && !strcmp(s->id, it->id)) {
873 s->used = t_now;
874 if (!s->failed || s->job || t_now - s->failed_at < ART_RETRY_US) {
875 if (failed) *failed = s->failed;
876 return s->img;
877 }
878 lru = i; // failed a while ago: try again in the same slot
879 break;
880 }
881 if (!s->job && (lru < 0 || s->used < icons[lru].used)) lru = i;
882 }
883 if (lru < 0 || icons_inflight >= ICON_INFLIGHT) return NULL;
884 icon_req_t *r = calloc(1, sizeof(*r));
885 job_t *j = r ? job_new(Icon_Run, Icon_Cleanup) : NULL;
886 if (!j) { free(r); return NULL; }
887 snprintf(r->url, sizeof(r->url), "%s%s", CATALOG_BASE, rel);
888 if (preview) store_preview_path(it, r->path, sizeof(r->path));
889 else store_icon_path(it, r->path, sizeof(r->path));
890 r->w = w;
891 r->h = h;
892 j->arg = r;
893 icon_slot_t *s = &icons[lru];
894 image_free(s->img);
895 memset(s, 0, sizeof(*s));
896 snprintf(s->id, sizeof(s->id), "%s", it->id);
897 s->w = w;
898 s->h = h;
899 s->preview = preview;
900 s->used = t_now;
901 s->job = j;
902 icons_inflight++;
903 job_post(j, LANE_ART);
904 return NULL;
905}
906
907static image_t *Icon_Get(const cat_item_t *it, int size) { return Art_Get(it, 0, size, size, NULL); }
908
909static void Placeholder(const char *title, int x, int y, int size)
910{
911 gfx_round_rect(x, y, size, size, size / 10, 0xff1a1c20);
912 char ch[2] = {Letter_Of(title), 0};
913 font_draw_center(FONT_BOLD, size / 2, x + size / 2, y + size / 4, ch, 0xff3a4152, 0);
914}
915
916static void Icon_Draw(const cat_item_t *it, int x, int y, int size)
917{
918 image_t *img = Icon_Get(it, size);
919 if (img) gfx_image(img, x, y, size, size, size / 10, 255);
920 else Placeholder(it->title, x, y, size);
921}
922
923// a theme's row: its preview, small and 16:9 (a square icon letterboxes it to nothing)
924#define THUMB_W 96
925#define THUMB_H 54
926
927static void Thumb_Draw(const cat_item_t *it, const char *title, int x, int y)
928{
929 image_t *img = it ? Art_Get(it, 1, THUMB_W, THUMB_H, NULL) : NULL;
930 if (img) {
931 gfx_image(img, x, y, THUMB_W, THUMB_H, 6, 255);
932 return;
933 }
934 gfx_round_rect(x, y, THUMB_W, THUMB_H, 6, 0xff1a1c20);
935 char ch[2] = {Letter_Of(title), 0};
936 font_draw_center(FONT_BOLD, 26, x + THUMB_W / 2, y + 12, ch, 0xff3a4152, 0);
937}
938
939/*
940==================
941Downloads: a queue, one at a time. Each entry keeps how it went (a finished
942one is dropped when the queue needs its place) for its detail screen.
943==================
944*/
945#define QUEUE_MAX 128 // (a big avatar pack fits)
946enum { Q_WAITING, Q_RUNNING, Q_DONE, Q_FAILED };
947
948typedef struct {
949 cat_item_t *it; // a copy
950 char dest[512];
951 int state, ok;
952 char msg[400]; // (Q_DONE, Q_FAILED) what happened
953} queued_t;
954
955static queued_t queue[QUEUE_MAX];
956static int queue_n;
957static cat_item_t *det; // the detail screen's item (a copy: its page may go)
958static download_t *dl; // the running one, queue[dl_q]
959static job_t *dl_job;
960static int dl_q = -1;
961static uint64_t dl_t_last;
962static long long dl_last_got;
963static double dl_speed; // bytes a second, smoothed
964static char result_msg[400]; // why the detail screen's item can't be queued (no profile...)
965static int result_ok;
966static int det_back = SCR_LIST; // where Circle goes from the detail screen
967static int preview_full; // the detail screen's preview, filling the screen (Triangle)
968static int big_asked; // Cross pressed once on a big download: the second press starts it
969static uint64_t dl_started; // when the running download started (for the speed it got)
970static double speed_seen; // bytes a second, from finished downloads (USRDIR/speed.txt)
971#define BIG_BYTES (1024LL * 1024 * 1024)
972
973// "about 40 minutes" for this many bytes at this speed; "" without one
974static void Fmt_Eta(long long bytes, double speed, char *out, int size)
975{
976 out[0] = 0;
977 if (speed <= 0) return;
978 long long min = (long long)(bytes / speed / 60 + 0.5);
979 if (min < 1) snprintf(out, size, "%s", tr("under a minute"));
980 else if (min < 90) snprintf(out, size, tr("about %d minutes"), (int)min);
981 else snprintf(out, size, tr("about %d hours %d minutes"), (int)(min / 60), (int)(min % 60));
982}
983
984// the same at the speed downloads have been getting ("" if none's finished yet)
985static void Fmt_Time(long long bytes, char *out, int size) { Fmt_Eta(bytes, speed_seen, out, size); }
986
987static void Speed_Load(void)
988{
989 char path[512];
990 snprintf(path, sizeof(path), "%s/speed.txt", plat_save_dir());
991 char *t = (char *)plat_read_file(path, NULL);
992 if (t) { speed_seen = atof(t); free(t); }
993 if (!(speed_seen > 0 && speed_seen < 1e9)) speed_seen = 0;
994}
995
996static void Speed_Save(void)
997{
998 char path[512], buf[32];
999 snprintf(path, sizeof(path), "%s/speed.txt", plat_save_dir());
1000 int n = snprintf(buf, sizeof(buf), "%.0f\n", speed_seen);
1001 plat_write_file(path, buf, n);
1002}
1003static long long free_bytes = -1; // on /dev_hdd0, read every few seconds (home and My downloads)
1004static uint64_t free_checked;
1005
1006static void Dl_Run(job_t *j) { download_run(j->arg); }
1007static void Profiles_Load(void);
1008
1009// the newest entry for an item, or -1
1010static int Queue_Find(const char *id)
1011{
1012 for (int i = queue_n - 1; i >= 0; i--)
1013 if (!strcmp(queue[i].it->id, id)) return i;
1014 return -1;
1015}
1016
1017// waiting or running
1018static int Queue_Active(int i) { return i >= 0 && i < queue_n && queue[i].state <= Q_RUNNING; }
1019static int Queue_Busy(void)
1020{
1021 int n = 0;
1022 for (int i = 0; i < queue_n; i++) n += Queue_Active(i);
1023 return n;
1024}
1025
1026static void Queue_Remove(int i)
1027{
1028 free(queue[i].it);
1029 memmove(&queue[i], &queue[i + 1], (queue_n - 1 - i) * sizeof(queued_t));
1030 queue_n--;
1031 if (dl_q > i) dl_q--;
1032}
1033
1034// the first waiting one, if nothing's running
1035static void Queue_Next(void)
1036{
1037 while (!dl) {
1038 int i;
1039 for (i = 0; i < queue_n && queue[i].state != Q_WAITING; i++);
1040 if (i == queue_n) return;
1041 queued_t *q = &queue[i];
1042 char dir[512];
1043 snprintf(dir, sizeof(dir), "%s", q->dest);
1044 char *slash = strrchr(dir, '/');
1045 if (slash) *slash = 0;
1046 if (!plat_mkdirs(dir)) {
1047 snprintf(q->msg, sizeof(q->msg), tr("Couldn't make the folder %s"), dir);
1048 q->state = Q_FAILED;
1049 continue;
1050 }
1051 download_t *d = calloc(1, sizeof(*d));
1052 job_t *j = d ? job_new(Dl_Run, NULL) : NULL;
1053 if (!j) {
1054 free(d);
1055 snprintf(q->msg, sizeof(q->msg), "%s", tr("Out of memory"));
1056 q->state = Q_FAILED;
1057 continue;
1058 }
1059 snprintf(d->url, sizeof(d->url), "%s%s", CATALOG_BASE, q->it->url);
1060 snprintf(d->dest, sizeof(d->dest), "%s", q->dest);
1061 d->size = q->it->size;
1062 memcpy(d->sha256, q->it->sha256, 32);
1063 j->arg = d;
1064 dl = d;
1065 dl_job = j;
1066 dl_q = i;
1067 q->state = Q_RUNNING;
1068 dl_last_got = 0;
1069 dl_speed = 0;
1070 dl_t_last = t_now;
1071 dl_started = t_now;
1072 job_post(j, LANE_DL);
1073 plat_log("download: %s -> %s", d->url, d->dest);
1074 }
1075 dirty = 1;
1076}
1077
1078// the item at the end of the queue (if it isn't in it already)
1079static void Queue_Add(const cat_item_t *it, const char *dest)
1080{
1081 int i = Queue_Find(it->id);
1082 if (Queue_Active(i)) return;
1083 if (i >= 0) Queue_Remove(i); // (how it went last time)
1084 for (i = 0; queue_n == QUEUE_MAX && i < queue_n; i++) // room: the oldest finished one goes
1085 if (!Queue_Active(i)) { Queue_Remove(i); break; }
1086 if (queue_n == QUEUE_MAX) {
1087 snprintf(result_msg, sizeof(result_msg), tr("%d downloads are waiting already: add this one when some have finished."), QUEUE_MAX);
1088 result_ok = 0;
1089 return;
1090 }
1091 queued_t *q = &queue[queue_n];
1092 memset(q, 0, sizeof(*q));
1093 if (!(q->it = catalog_item_copy(it))) return;
1094 snprintf(q->dest, sizeof(q->dest), "%s", dest);
1095 q->state = Q_WAITING;
1096 queue_n++;
1097 Queue_Next();
1098}
1099
1100// a waiting one leaves the queue; the running one stops (and what's arrived is deleted)
1101static void Queue_Cancel(int i)
1102{
1103 if (!Queue_Active(i)) return;
1104 if (queue[i].state == Q_RUNNING) dl->cancel = 1;
1105 else Queue_Remove(i);
1106 dirty = 1;
1107}
1108
1109// how many are ahead of it
1110static int Queue_Ahead(int i)
1111{
1112 int n = 0;
1113 for (int k = 0; k < i; k++) n += Queue_Active(k);
1114 return n;
1115}
1116
1117// the detail screen's item into the queue, once it's known where it goes
1118static void Download_Start(void)
1119{
1120 char dest[512];
1121 int code = store_dest(det, dest, sizeof(dest));
1122 if (code == DEST_PICK_PROFILE) {
1123 Profiles_Load();
1124 screen = SCR_PROFILE;
1125 dirty = 1;
1126 return;
1127 }
1128 result_msg[0] = 0;
1129 if (code != DEST_OK) {
1130 snprintf(result_msg, sizeof(result_msg), "%s", tr(store_dest_message(code)));
1131 result_ok = 0;
1132 dirty = 1;
1133 return;
1134 }
1135 // room for it, and for what's queued before it
1136 char dir[512], need_s[32], free_s[32], when[64];
1137 snprintf(dir, sizeof(dir), "%s", dest);
1138 char *slash = strrchr(dir, '/');
1139 if (slash) *slash = 0;
1140 long long need = det->size, fb = plat_free_bytes(dir[0] ? dir : "/dev_hdd0");
1141 for (int i = 0; i < queue_n; i++)
1142 if (Queue_Active(i)) need += queue[i].it->size - (i == dl_q && dl ? dl->got : 0);
1143 if (fb >= 0 && need > fb) {
1144 Fmt_Size(need, need_s, sizeof(need_s));
1145 Fmt_Size(fb, free_s, sizeof(free_s));
1146 snprintf(result_msg, sizeof(result_msg), tr("Not enough free space: this needs %s, and %s is free."), need_s, free_s);
1147 result_ok = 0;
1148 dirty = 1;
1149 return;
1150 }
1151 // a big one: say how big, and how long, and start it on the second press
1152 if (det->size >= BIG_BYTES && !big_asked) {
1153 Fmt_Size(det->size, need_s, sizeof(need_s));
1154 Fmt_Time(det->size, when, sizeof(when));
1155 if (when[0]) snprintf(result_msg, sizeof(result_msg), tr("This is %s (%s). Press X again to download it."), need_s, when);
1156 else snprintf(result_msg, sizeof(result_msg), tr("This is %s. Press X again to download it."), need_s);
1157 result_ok = 2;
1158 big_asked = 1;
1159 dirty = 1;
1160 return;
1161 }
1162 big_asked = 0;
1163 result_msg[0] = 0;
1164 Queue_Add(det, dest);
1165 dirty = 1;
1166}
1167
1168static void Detail_Open(const cat_item_t *it, int back)
1169{
1170 cat_item_t *c = catalog_item_copy(it);
1171 if (!c) return;
1172 free(det);
1173 det = c;
1174 result_msg[0] = 0; // (the last item's says nothing about this one)
1175 result_ok = 0;
1176 det_back = back;
1177 preview_full = 0;
1178 big_asked = 0;
1179 screen = SCR_DETAIL;
1180 dirty = 1;
1181}
1182
1183// a message in the header that goes with the next move (a pack queued, a file that wouldn't delete)
1184static void Note(const char *msg)
1185{
1186 snprintf(banner, sizeof(banner), "%s", msg);
1187 search_banner = 1;
1188 dirty = 1;
1189}
1190
1191/*
1192==================
1193Homebrew's descriptions in the UI's language (the catalogue's lang/<code>.json: id to text),
1194fetched and checked like a page; an item without one shows the English
1195==================
1196*/
1197static cJSON *desc_root; // the language in use's, or NULL
1198static unsigned char desc_sha[32], desc_want[32];
1199static job_t *desc_job;
1200static uint64_t desc_failed_at;
1201
1202static void Desc_Run(job_t *j)
1203{
1204 size_t len;
1205 char *t = store_get_file(j->arg, &len, &j->cancel, &j->ival, j->err, sizeof(j->err));
1206 if (!t) return;
1207 j->result = cJSON_ParseWithLength(t, len);
1208 free(t);
1209}
1210
1211static void Desc_Cleanup(job_t *j)
1212{
1213 free(j->arg);
1214 cJSON_Delete(j->result);
1215}
1216
1217static void Desc_Tick(void)
1218{
1219 if (desc_job) {
1220 if (!job_done(desc_job)) return;
1221 cJSON *r = desc_job->result;
1222 desc_job->result = NULL;
1223 if (r) {
1224 cJSON_Delete(desc_root);
1225 desc_root = r;
1226 memcpy(desc_sha, desc_want, 32);
1227 } else {
1228 plat_log("descriptions: %s", desc_job->err);
1229 desc_failed_at = t_now;
1230 }
1231 job_release(desc_job);
1232 desc_job = NULL;
1233 dirty = 1;
1234 return;
1235 }
1236 const cat_file_t *want = NULL;
1237 for (int i = 0; ix && i < ix->num_desc; i++)
1238 if (!strcmp(ix->desc[i].code, lang_code(lang_current()))) want = &ix->desc[i].file;
1239 if (!want) { // English, or none for this language
1240 if (desc_root) { cJSON_Delete(desc_root); desc_root = NULL; dirty = 1; }
1241 return;
1242 }
1243 if (desc_root && !memcmp(desc_sha, want->sha256, 32)) return;
1244 if (desc_failed_at && t_now - desc_failed_at < 30000000) return; // (tried lately)
1245 cat_file_t *f = malloc(sizeof(*f));
1246 job_t *j = f ? job_new(Desc_Run, Desc_Cleanup) : NULL;
1247 if (!j) { free(f); return; }
1248 *f = *want;
1249 memcpy(desc_want, want->sha256, 32);
1250 desc_failed_at = 0;
1251 j->arg = f;
1252 desc_job = j;
1253 job_post(j, LANE_API);
1254}
1255
1256static const char *Desc_Of(const cat_item_t *it)
1257{
1258 const cJSON *d = desc_root ? cJSON_GetObjectItemCaseSensitive(desc_root, it->id) : NULL;
1259 return cJSON_IsString(d) && d->valuestring[0] ? d->valuestring : it->description;
1260}
1261
1262/*
1263==================
1264Favourites: items starred on the detail screen (Select), kept in USRDIR/favourites.json
1265as a catalogue page, so they open and download without their own pages
1266==================
1267*/
1268static catalog_t *favs;
1269
1270static void Favs_Load(void)
1271{
1272 char path[512], err[160];
1273 size_t len;
1274 snprintf(path, sizeof(path), "%s/favourites.json", plat_save_dir());
1275 char *t = (char *)plat_read_file(path, &len);
1276 if (!t) return;
1277 catalog_t *c = catalog_parse_page(t, len, err, sizeof(err));
1278 free(t);
1279 if (c) { catalog_free(favs); favs = c; }
1280 else plat_log("favourites: %s", err);
1281}
1282
1283static int Fav_Find(const char *id)
1284{
1285 for (int i = 0; favs && i < favs->count; i++)
1286 if (!strcmp(favs->items[i].id, id)) return i;
1287 return -1;
1288}
1289
1290static cJSON *Fav_Json(const cat_item_t *it)
1291{
1292 cJSON *o = cJSON_CreateObject();
1293 char sha[65];
1294 for (int i = 0; i < 32; i++) snprintf(sha + i * 2, 3, "%02x", it->sha256[i]);
1295 cJSON_AddStringToObject(o, "id", it->id);
1296 cJSON_AddStringToObject(o, "type", catalog_type_name(it->type));
1297 cJSON_AddStringToObject(o, "title", it->title);
1298 cJSON_AddStringToObject(o, "author", it->author);
1299 cJSON_AddStringToObject(o, "version", it->version);
1300 cJSON_AddStringToObject(o, "description", it->description);
1301 cJSON_AddStringToObject(o, "group", it->group);
1302 cJSON_AddStringToObject(o, "url", it->url);
1303 cJSON_AddStringToObject(o, "icon", it->icon);
1304 cJSON_AddStringToObject(o, "preview", it->preview);
1305 cJSON_AddStringToObject(o, "titleId", it->title_id);
1306 if (it->updated) cJSON_AddNumberToObject(o, "updated", (double)it->updated);
1307 cJSON_AddNumberToObject(o, "size", (double)it->size);
1308 cJSON_AddStringToObject(o, "sha256", sha);
1309 cJSON_AddStringToObject(o, "requires", it->requires == REQ_CFW ? "cfw" : it->requires == REQ_HEN ? "hen" : "any");
1310 return o;
1311}
1312
1313// adds it (first), or takes it off if it's there; saved, then read back as the page it's kept as
1314static void Fav_Toggle(const cat_item_t *it)
1315{
1316 int had = Fav_Find(it->id);
1317 cJSON *root = cJSON_CreateObject(), *items = cJSON_CreateArray();
1318 if (!root || !items) { cJSON_Delete(root); cJSON_Delete(items); return; }
1319 cJSON_AddNumberToObject(root, "schema", CATALOG_SCHEMA);
1320 cJSON_AddItemToObject(root, "items", items);
1321 if (had < 0) cJSON_AddItemToArray(items, Fav_Json(it));
1322 for (int i = 0; favs && i < favs->count; i++)
1323 if (i != had) cJSON_AddItemToArray(items, Fav_Json(&favs->items[i]));
1324 char *t = cJSON_PrintUnformatted(root);
1325 cJSON_Delete(root);
1326 if (!t) return;
1327 char path[512], err[160];
1328 snprintf(path, sizeof(path), "%s/favourites.json", plat_save_dir());
1329 if (!plat_write_file(path, t, strlen(t))) plat_log("favourites: couldn't write %s", path);
1330 catalog_t *c = catalog_parse_page(t, strlen(t), err, sizeof(err));
1331 free(t);
1332 catalog_free(favs);
1333 favs = c;
1334 dirty = 1;
1335}
1336
1337/*
1338==================
1339A whole avatar pack: its pages fetched, then each avatar not already here queued
1340==================
1341*/
1342static cat_list_t pack; // the pack being queued (pages NULL: none)
1343static int pack_profile, pack_back; // the profile screen is up for it; the screen it came from
1344
1345static void Pack_End(void)
1346{
1347 catalog_free_list(&pack);
1348 pack_profile = 0;
1349}
1350
1351static void Pack_Start(const cat_list_t *l)
1352{
1353 Pack_End();
1354 cat_file_t *pages = malloc((l->num_pages ? l->num_pages : 1) * sizeof(cat_file_t));
1355 if (!pages) return;
1356 memcpy(pages, l->pages, l->num_pages * sizeof(cat_file_t));
1357 pack = *l;
1358 pack.pages = pages;
1359 pack.sorted = NULL; // (a pack's list has no other orders; this one owns nothing else)
1360 pack.num_sorted = 0;
1361 Note(tr("Getting the pack's avatars..."));
1362}
1363
1364static void Pack_Tick(void)
1365{
1366 if (!pack.pages || pack_profile) return;
1367 for (int p = 0; p < pack.num_pages; p++) {
1368 file_slot_t *fs = File_Get(&pack.pages[p], 0);
1369 if (fs && fs->failed) { Note(Error_Text(fs->err_kind, fs->err)); Pack_End(); return; }
1370 if (!fs || !fs->page) return;
1371 }
1372 char dest[512], msg[200];
1373 int added = 0;
1374 for (int p = 0; p < pack.num_pages; p++) {
1375 file_slot_t *fs = File_Get(&pack.pages[p], 0);
1376 if (!fs || !fs->page) return;
1377 for (int k = 0; k < fs->page->count; k++) {
1378 const cat_item_t *it = &fs->page->items[k];
1379 if (store_downloaded(it) || Queue_Active(Queue_Find(it->id))) continue;
1380 int code = store_dest(it, dest, sizeof(dest));
1381 if (code == DEST_PICK_PROFILE) { // which user first: then on from here
1382 Profiles_Load();
1383 pack_back = screen;
1384 pack_profile = 1;
1385 screen = SCR_PROFILE;
1386 dirty = 1;
1387 return;
1388 }
1389 if (code != DEST_OK) { Note(tr(store_dest_message(code))); Pack_End(); return; }
1390 Queue_Add(it, dest);
1391 if (!Queue_Active(Queue_Find(it->id))) { // the queue's full
1392 snprintf(msg, sizeof(msg), tr("%d avatars queued: the queue is full, so add the rest later."), added);
1393 Note(msg);
1394 Pack_End();
1395 return;
1396 }
1397 added++;
1398 }
1399 }
1400 if (added) snprintf(msg, sizeof(msg), tr("%d avatars queued: they download one after another."), added);
1401 else snprintf(msg, sizeof(msg), "%s", tr("Every avatar in this pack is downloaded or queued already."));
1402 Note(msg);
1403 Pack_End();
1404}
1405
1406static const char *Install_Hint(int type)
1407{
1408 switch (type) {
1409 case TYPE_HOMEBREW: return tr("Install it from the XMB: Package Manager > Install Package Files.");
1410 case TYPE_THEME: return tr("Choose it on the XMB: Settings > Theme Settings > Theme.");
1411 case TYPE_AVATAR: return tr("Choose it from the XMB: PlayStation Network > Account Management > Avatar.");
1412 }
1413 return "";
1414}
1415
1416static void Download_Tick(void)
1417{
1418 if (!dl) return;
1419 // progress: the speed over the last half second, smoothed
1420 if (t_now - dl_t_last >= 500000) {
1421 double inst = (dl->got - dl_last_got) * 1e6 / (double)(t_now - dl_t_last);
1422 if (dl->state == DL_RUNNING) dl_speed = dl_speed ? dl_speed * 0.6 + inst * 0.4 : inst;
1423 dl_last_got = dl->got;
1424 dl_t_last = t_now;
1425 dirty = 1;
1426 }
1427 if (!job_done(dl_job)) return;
1428 queued_t *q = &queue[dl_q];
1429 q->ok = 0;
1430 q->state = Q_FAILED;
1431 switch (dl->state) {
1432 case DL_DONE: {
1433 char size[32];
1434 Fmt_Size(dl->size, size, sizeof(size));
1435 store_mark_downloaded(q->it, dl->dest);
1436 // the speed it got (a long enough one), for the next big download's estimate
1437 double secs = (t_now - dl_started) / 1e6, got = (double)(dl->size - dl->resumed_from);
1438 if (secs > 10 && got > 5 * 1024 * 1024) {
1439 speed_seen = speed_seen > 0 ? speed_seen * 0.5 + got / secs * 0.5 : got / secs;
1440 Speed_Save();
1441 }
1442 snprintf(q->msg, sizeof(q->msg), tr("Checksum OK (%s). Saved to %s\n%s"), size, dl->dest, Install_Hint(q->it->type));
1443 q->ok = 1;
1444 q->state = Q_DONE;
1445 break;
1446 }
1447 case DL_CANCELLED:
1448 snprintf(q->msg, sizeof(q->msg), "%s", tr("Download cancelled."));
1449 break;
1450 case DL_PAUSED:
1451 snprintf(q->msg, sizeof(q->msg), "%s", tr("Download paused. Press X to resume."));
1452 break;
1453 default:
1454 snprintf(q->msg, sizeof(q->msg), "%s%s",
1455 dl->err_kind == ERR_CHECKSUM ? tr("The download didn't match its checksum, so it was deleted. Try again.") :
1456 Error_Text(dl->err_kind, dl->err),
1457 dl->err_kind == ERR_NETWORK ? tr("\nPress X to resume where it stopped.") : "");
1458 }
1459 if (dl->state != DL_DONE) // (done ones are logged by download.c, with their sha256)
1460 plat_log("download: %s %s at %lld of %lld bytes%s%s", dl->dest,
1461 dl->state == DL_CANCELLED ? "cancelled" : dl->state == DL_PAUSED ? "paused" : "failed",
1462 (long long)dl->got, dl->size, dl->err[0] ? ": " : "", dl->err);
1463 job_release(dl_job);
1464 dl_job = NULL;
1465 free(dl);
1466 dl = NULL;
1467 dl_q = -1;
1468 free_checked = 0; // (the space it took)
1469 Queue_Next();
1470 dirty = 1;
1471}
1472
1473/*
1474==================
1475Profiles (avatars)
1476==================
1477*/
1478static char profiles[16][64], profile_names[16][64];
1479static int num_profiles, profile_sel;
1480
1481static void Profiles_Load(void)
1482{
1483 num_profiles = store_profiles(profiles, 16);
1484 for (int i = 0; i < num_profiles; i++) {
1485 char path[128];
1486 snprintf(path, sizeof(path), "/dev_hdd0/home/%s/localusername", profiles[i]);
1487 char *name = (char *)plat_read_file(path, NULL);
1488 snprintf(profile_names[i], 64, "%s", name && name[0] ? name : profiles[i]);
1489 free(name);
1490 }
1491 profile_sel = 0;
1492}
1493
1494/*
1495==================
1496My downloads: the queue (what hasn't finished), then what's on the HDD, newest first
1497==================
1498*/
1499static int dls_sel, dls_top, dls_confirm = -1; // dls_confirm: the row Square was pressed on once
1500static int dls_tab; // 0 the downloads, 1 the favourites (L1/R1)
1501
1502static int Dls_Queue_Rows(void)
1503{
1504 int n = 0;
1505 for (int i = 0; i < queue_n; i++) n += queue[i].state != Q_DONE;
1506 return n;
1507}
1508
1509// a queue row's entry
1510static int Dls_Queue_At(int row)
1511{
1512 for (int i = 0; i < queue_n; i++)
1513 if (queue[i].state != Q_DONE && row-- == 0) return i;
1514 return -1;
1515}
1516
1517static int Dls_Count(void) { return Dls_Queue_Rows() + store_num_downloads(); }
1518
1519// a homebrew item in the catalogue by id (its pages asked for as needed): NULL if it isn't (yet)
1520static const cat_item_t *Homebrew_Find(const char *id)
1521{
1522 if (strncmp(id, "hb-", 3)) return NULL; // (only homebrew gets new versions)
1523 for (int l = 0; ix && l < ix->num_lists[TYPE_HOMEBREW]; l++)
1524 for (int p = 0; p < ix->lists[TYPE_HOMEBREW][l].num_pages; p++) {
1525 file_slot_t *s = File_Get(&ix->lists[TYPE_HOMEBREW][l].pages[p], 0);
1526 if (!s || !s->page) continue;
1527 for (int k = 0; k < s->page->count; k++)
1528 if (!strcmp(s->page->items[k].id, id)) return &s->page->items[k];
1529 }
1530 return NULL;
1531}
1532
1533// the catalogue has a different file for a download (a new version)
1534static int Dls_Newer(const store_rec_t *r)
1535{
1536 const cat_item_t *it = Homebrew_Find(r->id);
1537 char sha[65];
1538 if (!it) return 0;
1539 for (int i = 0; i < 32; i++) snprintf(sha + i * 2, 3, "%02x", it->sha256[i]);
1540 return strcmp(sha, r->sha) != 0;
1541}
1542
1543static void Input_Downloads(uint32_t p)
1544{
1545 int n = Dls_Count(), qrows = Dls_Queue_Rows();
1546 if (p & BTN_CIRCLE) {
1547 dls_confirm = -1;
1548 Search_Banner_Clear();
1549 screen = SCR_HOME;
1550 dirty = 1;
1551 return;
1552 }
1553 if (p & (BTN_L1 | BTN_R1)) {
1554 dls_tab ^= 1;
1555 dls_sel = dls_top = 0;
1556 dls_confirm = -1;
1557 dirty = 1;
1558 return;
1559 }
1560 if (dls_tab == 1) { // favourites: Cross opens one, Square takes it off
1561 n = favs ? favs->count : 0;
1562 if (p & BTN_DOWN) dls_sel++;
1563 if (p & BTN_UP) dls_sel--;
1564 if (p & BTN_RIGHT) dls_sel += ROWS;
1565 if (p & BTN_LEFT) dls_sel -= ROWS;
1566 if (dls_sel >= n) dls_sel = n - 1;
1567 if (dls_sel < 0) dls_sel = 0;
1568 if ((p & BTN_CROSS) && n) Detail_Open(&favs->items[dls_sel], SCR_DOWNLOADS);
1569 else if ((p & BTN_SQUARE) && n) Fav_Toggle(&favs->items[dls_sel]);
1570 n = favs ? favs->count : 0;
1571 if (dls_sel >= n) dls_sel = n - 1;
1572 if (dls_sel < 0) dls_sel = 0;
1573 if (dls_top > dls_sel) dls_top = dls_sel;
1574 if (dls_top < dls_sel - ROWS + 1) dls_top = dls_sel - ROWS + 1;
1575 if (p) dirty = 1;
1576 return;
1577 }
1578 if (p & ~BTN_SQUARE) dls_confirm = -1; // (anything else and the delete's off)
1579 if (p & BTN_DOWN) dls_sel++;
1580 if (p & BTN_UP) dls_sel--;
1581 if (p & BTN_RIGHT) dls_sel += ROWS;
1582 if (p & BTN_LEFT) dls_sel -= ROWS;
1583 if (dls_sel >= n) dls_sel = n - 1;
1584 if (dls_sel < 0) dls_sel = 0;
1585 if (p & BTN_CROSS) {
1586 store_rec_t r;
1587 const cat_item_t *it;
1588 if (dls_sel < qrows) Detail_Open(queue[Dls_Queue_At(dls_sel)].it, SCR_DOWNLOADS);
1589 else if (store_download_at(dls_sel - qrows, &r) && (it = Homebrew_Find(r.id)))
1590 Detail_Open(it, SCR_DOWNLOADS);
1591 }
1592 if ((p & BTN_SQUARE) && dls_sel < qrows) {
1593 int q = Dls_Queue_At(dls_sel);
1594 if (Queue_Active(q)) Queue_Cancel(q);
1595 else if (q >= 0) Queue_Remove(q); // a failed one: off the list
1596 } else if ((p & BTN_SQUARE) && dls_sel < n) {
1597 // twice to delete: the first press asks
1598 if (dls_confirm == dls_sel) {
1599 if (!store_delete_download(dls_sel - qrows))
1600 Note(tr("Couldn't delete that file"));
1601 dls_confirm = -1;
1602 free_checked = 0;
1603 } else
1604 dls_confirm = dls_sel;
1605 }
1606 n = Dls_Count();
1607 if (dls_sel >= n) dls_sel = n - 1;
1608 if (dls_sel < 0) dls_sel = 0;
1609 if (dls_top > dls_sel) dls_top = dls_sel;
1610 if (dls_top < dls_sel - ROWS + 1) dls_top = dls_sel - ROWS + 1;
1611 if (p) dirty = 1;
1612}
1613
1614/*
1615==================
1616The app's own update, from the catalogue
1617==================
1618*/
1619static int Version_Newer(const char *a, const char *b)
1620{
1621 int x[3] = {0}, y[3] = {0};
1622 sscanf(a, "%d.%d.%d", &x[0], &x[1], &x[2]);
1623 sscanf(b, "%d.%d.%d", &y[0], &y[1], &y[2]);
1624 for (int i = 0; i < 3; i++)
1625 if (x[i] != y[i]) return x[i] > y[i];
1626 return 0;
1627}
1628
1629static int Update_Available(void) { return ix && ix->app.version[0] && Version_Newer(ix->app.version, APP_VERSION); }
1630
1631// the update, as a homebrew item on the detail screen
1632static void Update_Open(void)
1633{
1634 char title[64];
1635 snprintf(title, sizeof(title), APP_NAME " %s", ix->app.version);
1636 cat_item_t it;
1637 memset(&it, 0, sizeof(it));
1638 it.id = "ps3world-app";
1639 it.title = title;
1640 it.author = "PS3 World";
1641 it.version = ix->app.version;
1642 it.description = tr("A new version of this app. Once it's downloaded, install it from Package Manager > Install Package Files: "
1643 "it installs over this one.");
1644 it.group = it.icon = it.preview = it.list = it.title_id = ""; // (every string set: catalog_item_copy reads them all)
1645 it.url = ix->app.file.url;
1646 it.size = ix->app.size;
1647 memcpy(it.sha256, ix->app.file.sha256, 32);
1648 it.type = TYPE_HOMEBREW;
1649 it.requires = REQ_HEN;
1650 Detail_Open(&it, SCR_HOME);
1651}
1652
1653/*
1654==================
1655Input
1656==================
1657*/
1658static int searching;
1659static int sound_on = 1; // (USRDIR/sound.txt: "off" turns them off)
1660
1661static void Sound_Load(void)
1662{
1663 char path[512];
1664 snprintf(path, sizeof(path), "%s/sound.txt", plat_save_dir());
1665 char *t = (char *)plat_read_file(path, NULL);
1666 if (t) { sound_on = strncmp(t, "off", 3) != 0; free(t); }
1667 plat_sound_enable(sound_on);
1668}
1669
1670static void Sound_Set(int on)
1671{
1672 char path[512];
1673 sound_on = on;
1674 plat_sound_enable(on);
1675 snprintf(path, sizeof(path), "%s/sound.txt", plat_save_dir());
1676 plat_write_file(path, on ? "on\n" : "off\n", on ? 3 : 4);
1677}
1678static int lang_sel; // the language screen's row: 0 the PS3's, then LANG_* + 1
1679
1680static void Letter_Jump(view_t *v, const cat_list_t *l, int dir)
1681{
1682 int pos = v->sel, slot = 0;
1683 for (int i = 0; i < NUM_LETTERS; i++) // the letter we're in: the last one starting at or before pos
1684 if (l->letter_first[i] >= 0 && l->letter_first[i] <= pos) slot = i;
1685 if (dir > 0) {
1686 for (int i = slot + 1; i < NUM_LETTERS; i++)
1687 if (l->letter_first[i] >= 0) { v->sel = l->letter_first[i]; break; }
1688 } else {
1689 // to the start of this letter, or of the one before when already there
1690 if (l->letter_first[slot] >= 0 && l->letter_first[slot] < pos) v->sel = l->letter_first[slot];
1691 else for (int i = slot - 1; i >= 0; i--)
1692 if (l->letter_first[i] >= 0) { v->sel = l->letter_first[i]; break; }
1693 }
1694 v->top = v->sel;
1695}
1696
1697// Homebrew's few categories are big cards, three a row; Themes' and Avatars' are tiles, TILE_COLS a row
1698static int Picker_Cols(void) { return cur == TYPE_HOMEBREW ? 3 : TILE_COLS; }
1699static int Picker_Rows_Shown(void) { return cur == TYPE_HOMEBREW ? 99 : (CARD_H + CARD_GAP) / (TILE_H + CARD_GAP); }
1700
1701// out of the Recently added lists: each section back to its categories (or its one list)
1702static void Recent_Leave(void)
1703{
1704 for (int t = 0; t < NUM_TYPES; t++)
1705 if (cur_list[t] >= 0 && cur_list[t] == Recent_Of(t)) cur_list[t] = Cats(t) > 1 ? -1 : Cat_List(t, 0);
1706 recent_mode = 0;
1707}
1708
1709static void Open_Section(int t)
1710{
1711 Search_Banner_Clear();
1712 Folder_Close();
1713 Recent_Leave();
1714 cur = t;
1715 screen = SCR_LIST;
1716 if (Cats(t) > 1) { // its categories first, on the last one opened
1717 picker_sel = cur_list[t] > 0 ? List_Cat(t, cur_list[t]) : 0;
1718 picker_top = 0;
1719 cur_list[t] = -1;
1720 }
1721 dirty = 1;
1722}
1723
1724static void Section_Step(int dir)
1725{
1726 Open_Section((cur + NUM_TYPES + dir) % NUM_TYPES);
1727}
1728
1729// the next section from t (dir 1 or -1) with a Recently added list: -1 if none has one
1730static int Recent_Next(int t, int dir)
1731{
1732 for (int k = 0; k < NUM_TYPES; k++) {
1733 t = (t + NUM_TYPES + dir) % NUM_TYPES;
1734 if (Recent_Of(t) >= 0) return t;
1735 }
1736 return -1;
1737}
1738
1739static void Open_Recent(int t)
1740{
1741 Search_Banner_Clear();
1742 Folder_Close();
1743 Recent_Leave();
1744 cur = t;
1745 cur_list[t] = Recent_Of(t);
1746 recent_mode = 1;
1747 screen = SCR_LIST;
1748 dirty = 1;
1749}
1750
1751static void Recent_Home(void)
1752{
1753 Recent_Leave();
1754 home_sel = HOME_RECENT;
1755 screen = SCR_HOME;
1756 dirty = 1;
1757}
1758
1759// in a list of a section with categories: the next one (round from the last to the first)
1760static void List_Step(int dir)
1761{
1762 int n = ix->num_lists[cur], r = Recent_Of(cur);
1763 Search_Banner_Clear();
1764 Folder_Close();
1765 do cur_list[cur] = (cur_list[cur] + n + dir) % n; while (cur_list[cur] == r);
1766 dirty = 1;
1767}
1768
1769static uint64_t quit_asked; // when Circle was pressed on the home screen (again soon after: quit)
1770
1771static void Input_Home(uint32_t p)
1772{
1773 if (p & BTN_CIRCLE) {
1774 if (quit_asked && t_now - quit_asked < 4000000) { plat_quit(); return; }
1775 quit_asked = t_now;
1776 Note(tr(dl ? "Press again to quit to the XMB. The download picks up where it left off next time." :
1777 "Press again to quit to the XMB."));
1778 return;
1779 }
1780 if (p && quit_asked) {
1781 quit_asked = 0;
1782 Search_Banner_Clear();
1783 }
1784 // Homebrew over Favourites on the left; Themes, Avatars and Recently added down the right
1785 int left = home_sel == TYPE_HOMEBREW || home_sel == HOME_FAVS;
1786 if ((p & BTN_RIGHT) && left) home_sel = home_sel == HOME_FAVS ? HOME_RECENT : TYPE_THEME;
1787 else if ((p & BTN_LEFT) && !left) home_sel = home_sel == HOME_RECENT ? HOME_FAVS : TYPE_HOMEBREW;
1788 else if (p & BTN_DOWN)
1789 home_sel = home_sel == TYPE_HOMEBREW ? HOME_FAVS : home_sel == TYPE_THEME ? TYPE_AVATAR : home_sel == TYPE_AVATAR ? HOME_RECENT : home_sel;
1790 else if (p & BTN_UP)
1791 home_sel = home_sel == HOME_FAVS ? TYPE_HOMEBREW : home_sel == HOME_RECENT ? TYPE_AVATAR : home_sel == TYPE_AVATAR ? TYPE_THEME : home_sel;
1792 if ((p & BTN_CROSS) && home_sel == HOME_RECENT) { // Homebrew's first (L1/R1 for the others')
1793 int t = Recent_Of(TYPE_HOMEBREW) >= 0 ? TYPE_HOMEBREW : Recent_Next(TYPE_HOMEBREW, 1);
1794 if (t >= 0) Open_Recent(t);
1795 } else if ((p & BTN_CROSS) && home_sel == HOME_FAVS) {
1796 dls_tab = 1;
1797 dls_sel = dls_top = 0;
1798 dls_confirm = -1;
1799 screen = SCR_DOWNLOADS;
1800 } else if (p & BTN_CROSS) Open_Section(home_sel);
1801 if (p & BTN_TRIANGLE) {
1802 dls_sel = dls_top = 0;
1803 dls_confirm = -1;
1804 screen = SCR_DOWNLOADS;
1805 }
1806 if ((p & BTN_SQUARE) && Update_Available()) Update_Open();
1807 if (p & BTN_SELECT) {
1808 lang_sel = i18n_choice() + 1;
1809 screen = SCR_LANG;
1810 }
1811 if (p & BTN_START) screen = SCR_CREDITS;
1812 if (p) dirty = 1;
1813}
1814
1815/*
1816==================
1817Language: the PS3's (the first row), or one of ours
1818==================
1819*/
1820/*
1821==================
1822What's new: this version's changes, once, the first time it's opened after an update
1823==================
1824*/
1825static const char *const news_lines[] = { // (1.2.2's)
1826 "Homebrew you've installed says Update when a newer version is out.",
1827};
1828#define NUM_NEWS ((int)(sizeof(news_lines) / sizeof(news_lines[0])))
1829
1830static void News_Seen(void)
1831{
1832 char path[512];
1833 snprintf(path, sizeof(path), "%s/seen.txt", plat_save_dir());
1834 plat_write_file(path, APP_VERSION "\n", strlen(APP_VERSION) + 1);
1835 news_due = 0;
1836}
1837
1838// due when the last version shown isn't this one; a first install (never run) has nothing new, only marked seen
1839static void News_Check(int run_before)
1840{
1841 char path[512];
1842 snprintf(path, sizeof(path), "%s/seen.txt", plat_save_dir());
1843 char *t = (char *)plat_read_file(path, NULL);
1844 if (t) news_due = strncmp(t, APP_VERSION "\n", strlen(APP_VERSION) + 1) != 0;
1845 else if (run_before) news_due = 1;
1846 else News_Seen();
1847 free(t);
1848 if (news_due) plat_log("ui: What's new in %s, the first time", APP_VERSION);
1849}
1850
1851static void Input_News(uint32_t p)
1852{
1853 if (p & (BTN_CROSS | BTN_CIRCLE | BTN_START)) {
1854 News_Seen();
1855 screen = SCR_HOME;
1856 }
1857 if (p) dirty = 1;
1858}
1859
1860static void Input_Credits(uint32_t p)
1861{
1862 if (p & (BTN_CIRCLE | BTN_START | BTN_CROSS)) screen = SCR_HOME;
1863 if (p) dirty = 1;
1864}
1865
1866// lang_sel: 0 the PS3's language, 1..LANG_COUNT a language (in a grid, LG_COLS a row), then the sounds
1867static void Input_Lang(uint32_t p)
1868{
1869 int n = LANG_COUNT, last_row = (n - 1) / LG_COLS * LG_COLS;
1870 if (lang_sel == 0) {
1871 if (p & BTN_DOWN) lang_sel = 1;
1872 } else if (lang_sel == n + 1) {
1873 if (p & BTN_UP) lang_sel = last_row + 1;
1874 } else {
1875 int i = lang_sel - 1, col = i % LG_COLS;
1876 if ((p & BTN_LEFT) && col > 0) i--;
1877 else if ((p & BTN_RIGHT) && col < LG_COLS - 1 && i + 1 < n) i++;
1878 if (p & BTN_UP) i = i >= LG_COLS ? i - LG_COLS : -1;
1879 else if (p & BTN_DOWN) i = i + LG_COLS < n ? i + LG_COLS : (i >= last_row ? n : n - 1);
1880 lang_sel = i + 1;
1881 }
1882 if (p & BTN_CIRCLE) screen = SCR_HOME;
1883 if ((p & BTN_CROSS) && lang_sel == LANG_COUNT + 1) Sound_Set(!sound_on); // (the last row)
1884 else if (p & BTN_CROSS) {
1885 i18n_set(lang_sel - 1);
1886 font_flush(); // (another language may draw characters from other fonts)
1887 screen = SCR_HOME;
1888 }
1889 if (p) dirty = 1;
1890}
1891
1892/*
1893==================
1894Surprise me: something at random from the section (its whole list, or for homebrew any of its
1895categories by their size); opened when its page is here
1896==================
1897*/
1898static int surprise_list = -1, surprise_pos;
1899
1900static void Surprise_Start(void)
1901{
1902 int n = ix->num_lists[cur], l = -1, total = 0;
1903 if (n && !strncmp(ix->lists[cur][0].name, "All ", 4)) l = 0; // themes, avatars: all of them
1904 else {
1905 for (int i = 0; i < n; i++)
1906 if (strcmp(ix->lists[cur][i].name, "Recently added")) total += ix->lists[cur][i].count;
1907 if (total <= 0) return;
1908 int r = rand() % total;
1909 for (int i = 0; i < n && l < 0; i++) {
1910 if (!strcmp(ix->lists[cur][i].name, "Recently added")) continue;
1911 if (r < ix->lists[cur][i].count) { l = i; surprise_pos = r; }
1912 else r -= ix->lists[cur][i].count;
1913 }
1914 }
1915 if (l < 0 || ix->lists[cur][l].count <= 0) return;
1916 if (l == 0 && !strncmp(ix->lists[cur][0].name, "All ", 4)) surprise_pos = rand() % ix->lists[cur][0].count;
1917 surprise_list = l;
1918 Note(tr("Picking something at random..."));
1919}
1920
1921static void Surprise_Tick(void)
1922{
1923 if (surprise_list < 0) return;
1924 cat_list_t *l = List(cur, surprise_list);
1925 if (!l || screen != SCR_LIST || !Picking()) { surprise_list = -1; return; }
1926 file_slot_t *slot;
1927 const cat_item_t *it = Item_At(l, surprise_pos, &slot);
1928 if (slot && slot->failed) { Note(Error_Text(slot->err_kind, slot->err)); surprise_list = -1; return; }
1929 if (!it) return;
1930 Search_Banner_Clear();
1931 if (it->list[0]) { // an avatar pack: its list, at it, opened
1932 cat_list_t f;
1933 cur_list[cur] = surprise_list;
1934 view_t *v = Cur_View();
1935 if (v) { v->sort = SORT_AZ; v->sel = v->top = surprise_pos; } // (the position is in A-Z order)
1936 if (catalog_parse_list(it->list, &f)) { Folder_Close(); folder = f; }
1937 } else
1938 Detail_Open(it, SCR_LIST); // (Circle: back to the categories)
1939 surprise_list = -1;
1940 dirty = 1;
1941}
1942
1943static void Input_Picker(uint32_t p)
1944{
1945 if (p & BTN_SQUARE) { Surprise_Start(); return; }
1946 int n = Cats(cur), cols = Picker_Cols(), shown = Picker_Rows_Shown();
1947 if (p & BTN_R1) { Section_Step(1); return; }
1948 if (p & BTN_L1) { Section_Step(-1); return; }
1949 if (p & BTN_CIRCLE) {
1950 home_sel = cur;
1951 screen = SCR_HOME;
1952 dirty = 1;
1953 return;
1954 }
1955 if (p & BTN_RIGHT) picker_sel++;
1956 if (p & BTN_LEFT) picker_sel--;
1957 if ((p & BTN_DOWN) && picker_sel + cols < n) picker_sel += cols;
1958 else if ((p & BTN_DOWN) && picker_sel / cols < (n - 1) / cols) picker_sel = n - 1; // onto a shorter last row
1959 if ((p & BTN_UP) && picker_sel >= cols) picker_sel -= cols;
1960 if (picker_sel >= n) picker_sel = n - 1;
1961 if (picker_sel < 0) picker_sel = 0;
1962 int row = picker_sel / cols;
1963 if (row < picker_top) picker_top = row;
1964 if (row >= picker_top + shown) picker_top = row - shown + 1;
1965 if (p & BTN_CROSS) cur_list[cur] = Cat_List(cur, picker_sel);
1966 if (p) dirty = 1;
1967}
1968
1969static void Input_List(uint32_t p)
1970{
1971 cat_list_t *l = Cur_List();
1972 view_t *v = Cur_View();
1973 // L1/R1: the section's categories in a list (its sections from the categories screen)
1974 int step = (p & BTN_R1) ? 1 : (p & BTN_L1) ? -1 : 0;
1975 if (step && recent_mode) { // (from the home screen's card: the sections' Recently added)
1976 int t = Recent_Next(cur, step);
1977 if (t >= 0 && t != cur) Open_Recent(t);
1978 return;
1979 }
1980 if (step && l && Cats(cur) > 1) { List_Step(step); return; }
1981 if (step) { Section_Step(step); return; }
1982 if (!l || !v) { // an empty section: only the way out
1983 if ((p & BTN_CIRCLE) && recent_mode) Recent_Home();
1984 else if (p & BTN_CIRCLE) {
1985 home_sel = cur;
1986 screen = SCR_HOME;
1987 dirty = 1;
1988 }
1989 return;
1990 }
1991 int count = View_Count(v, l);
1992 if (Gridded(cur)) {
1993 int cols = Grid_Cols(cur);
1994 if (p & BTN_RIGHT) v->sel++;
1995 if (p & BTN_LEFT) v->sel--;
1996 if ((p & BTN_DOWN) && v->sel + cols < count) v->sel += cols;
1997 else if ((p & BTN_DOWN) && v->sel / cols < (count - 1) / cols) v->sel = count - 1; // onto a shorter last row
1998 if ((p & BTN_UP) && v->sel >= cols) v->sel -= cols;
1999 } else {
2000 if (p & BTN_DOWN) v->sel++;
2001 if (p & BTN_UP) v->sel--;
2002 if (p & BTN_RIGHT) v->sel += ROWS;
2003 if (p & BTN_LEFT) v->sel -= ROWS;
2004 }
2005 if (!v->results && !v->sort && (p & BTN_R2)) Letter_Jump(v, l, 1);
2006 if (!v->results && !v->sort && (p & BTN_L2)) Letter_Jump(v, l, -1);
2007 cat_list_t *base = Base_List();
2008 if ((p & BTN_SELECT) && base && base->num_sorted) {
2009 v->sort = (v->sort + 1) % (base->num_sorted + 1);
2010 free(v->results);
2011 v->results = NULL;
2012 v->query[0] = 0;
2013 v->search_wait = 0;
2014 v->sel = v->top = 0;
2015 Search_Banner_Clear();
2016 dirty = 1;
2017 return;
2018 }
2019 if (p & BTN_TRIANGLE) {
2020 char title[64];
2021 snprintf(title, sizeof(title), "%s %s%s%s", tr("Search"), tr(section_names[cur]), l->name[0] ? ": " : "", tr(l->name));
2022 if (plat_osk_open(title, v->query)) searching = 1;
2023 }
2024 if ((p & (BTN_SQUARE | BTN_CIRCLE)) && (v->query[0] || v->search_wait)) {
2025 Search_Banner_Clear();
2026 v->query[0] = 0;
2027 v->search_wait = 0;
2028 free(v->results);
2029 v->results = NULL;
2030 v->sel = v->top = 0;
2031 dirty = 1;
2032 return;
2033 }
2034 if ((p & BTN_SQUARE) && cur == TYPE_AVATAR) { // a whole pack into the queue
2035 if (folder.pages) Pack_Start(&folder);
2036 else {
2037 file_slot_t *slot;
2038 const cat_item_t *it = Item_At(l, View_Pos(v, v->sel), &slot);
2039 cat_list_t f;
2040 if (it && it->list[0] && catalog_parse_list(it->list, &f)) {
2041 Pack_Start(&f);
2042 catalog_free_list(&f);
2043 }
2044 }
2045 dirty = 1;
2046 return;
2047 }
2048 if ((p & BTN_CIRCLE) && folder.pages) { // out of the pack, back to the packs
2049 Folder_Close();
2050 dirty = 1;
2051 return;
2052 }
2053 if ((p & BTN_CIRCLE) && recent_mode) { // back to the home screen's card
2054 Recent_Home();
2055 return;
2056 }
2057 if ((p & BTN_CIRCLE) && Cats(cur) > 1) { // back to the section's categories
2058 picker_sel = List_Cat(cur, cur_list[cur]);
2059 picker_top = picker_sel / Picker_Cols() - Picker_Rows_Shown() + 1;
2060 if (picker_top < 0) picker_top = 0;
2061 cur_list[cur] = -1;
2062 dirty = 1;
2063 return;
2064 }
2065 if (p & BTN_CIRCLE) { // back to the home cards
2066 home_sel = cur;
2067 screen = SCR_HOME;
2068 dirty = 1;
2069 return;
2070 }
2071 if ((p & BTN_CROSS) && count) {
2072 file_slot_t *slot;
2073 const cat_item_t *it = Item_At(l, View_Pos(v, v->sel), &slot);
2074 if (it && it->list[0]) { // a pack: its avatars, as a list
2075 cat_list_t f;
2076 if (catalog_parse_list(it->list, &f)) {
2077 Folder_Close();
2078 folder = f;
2079 } else
2080 plat_log("catalog: %s's folder didn't check out", it->id);
2081 dirty = 1;
2082 return;
2083 }
2084 if (it) Detail_Open(it, SCR_LIST);
2085 }
2086 Clamp(v, View_Count(v, l));
2087 if (p) dirty = 1;
2088}
2089
2090static void Leave_Detail(void)
2091{
2092 free(det);
2093 det = NULL;
2094 screen = det_back;
2095 if (pending) {
2096 Index_Use(pending);
2097 pending = NULL;
2098 }
2099 dirty = 1;
2100}
2101
2102static void Input_Detail(uint32_t p)
2103{
2104 int q = det ? Queue_Find(det->id) : -1;
2105 if (preview_full) {
2106 if (p & (BTN_CIRCLE | BTN_TRIANGLE | BTN_CROSS)) preview_full = 0;
2107 if (p) dirty = 1;
2108 return;
2109 }
2110 if ((p & BTN_TRIANGLE) && det && det->preview[0]) { preview_full = 1; dirty = 1; return; }
2111 if (p & BTN_CIRCLE) { Leave_Detail(); return; } // (a download carries on)
2112 if ((p & BTN_SELECT) && det && strcmp(det->id, "ps3world-app")) Fav_Toggle(det);
2113 if ((p & BTN_SQUARE) && Queue_Active(q)) Queue_Cancel(q);
2114 else if ((p & BTN_CROSS) && det && !Queue_Active(q)) Download_Start();
2115 if (p) dirty = 1;
2116}
2117
2118static void Input_Profile(uint32_t p)
2119{
2120 if (p & BTN_DOWN && profile_sel < num_profiles - 1) profile_sel++;
2121 if (p & BTN_UP && profile_sel > 0) profile_sel--;
2122 if (p & BTN_CIRCLE) {
2123 if (pack_profile) { Pack_End(); screen = pack_back; }
2124 else screen = SCR_DETAIL;
2125 }
2126 if (p & BTN_CROSS && num_profiles) {
2127 store_set_avatar_profile(profiles[profile_sel]);
2128 if (pack_profile) { pack_profile = 0; screen = pack_back; } // (Pack_Tick goes on)
2129 else { screen = SCR_DETAIL; Download_Start(); }
2130 }
2131 if (p) dirty = 1;
2132}
2133
2134/*
2135==================
2136Frame
2137==================
2138*/
2139void ui_init(void)
2140{
2141 i18n_init();
2142 srand((unsigned)plat_time_us());
2143 Speed_Load();
2144 Sound_Load();
2145 char name[64];
2146 backdrop = Load_Image("backdrop.png", SCREEN_W, SCREEN_H);
2147 logo = Load_Image("logo.png", 220, 31);
2148 logo_big = Load_Image("logo.png", INTRO_LOGO_W, INTRO_LOGO_H);
2149 qr_contact = Load_Image("qr-contact.png", QR_PX, QR_PX);
2150 for (int i = 0; i < LANG_COUNT; i++) {
2151 char f[64];
2152 snprintf(f, sizeof(f), "flags/%s.png", lang_flag(i));
2153 flag_img[i] = Load_Image(f, 56, 42);
2154 snprintf(f, sizeof(f), "%s.png", lang_code(i));
2155 name_img[i][0] = Load_Name(f, &name_w[i]);
2156 snprintf(f, sizeof(f), "%s-on.png", lang_code(i));
2157 if (name_img[i][0]) name_img[i][1] = Load_Name(f, &name_w[i]);
2158 }
2159 for (int i = 0; i < NUM_GLYPHS; i++) {
2160 snprintf(name, sizeof(name), "glyphs/%s.png", glyph_files[i]);
2161 glyphs[i] = Load_Image(name, GLYPH, GLYPH); // (at the screen's size: Load_Image sees to it)
2162 }
2163 // the cards' art, at the sizes Draw_Home and Draw_Picker draw them (tools/make-art.mjs makes them so)
2164 card_bg = Load_Image("cards/bg.png", 256, 256);
2165 for (int t = 0; t < NUM_TYPES; t++) {
2166 snprintf(name, sizeof(name), "cards/%s.png", home_cards[t].art);
2167 home_art[t] = Load_Image(name, t == TYPE_HOMEBREW ? LIST_W - HOME_RW - CARD_GAP : HOME_RW, t == TYPE_HOMEBREW ? HOME_H : (HOME_H - CARD_GAP) / 2);
2168 }
2169 for (int i = 0; i < NUM_CAT_CARDS; i++) {
2170 snprintf(name, sizeof(name), "cards/%s.png", cat_cards[i].art);
2171 cat_art[i] = Load_Image(name, CAT_W, (CARD_H - CARD_GAP) / 2);
2172 }
2173 store_init();
2174 Favs_Load();
2175 cat_index_t *c = store_load_index();
2176 News_Check(c != NULL || store_num_downloads() > 0); // (a cached catalogue or a download: it's been run before)
2177 if (c) {
2178 plat_log("catalog: index from the HDD cache (generated %s)", c->generated);
2179 Index_Use(c);
2180 }
2181 Fetch_Start(); // the server's copy, in the background
2182}
2183
2184// the New ticker (drawn with the home screen, below)
2185static void Ticker_Build(void);
2186static int Ticker_Scrolls(void);
2187static uint64_t ticker_at; // when it was last drawn (it's redrawn as it moves)
2188
2189/*
2190==================
2191The intro: the logo eases up and in over the backdrop, a glow passes behind it and a line draws out
2192under it, then it all fades into the app (which loads underneath meanwhile). Any button hurries it on
2193to the fade.
2194==================
2195*/
2196static uint64_t intro_t0; // its first frame (0: not drawn yet)
2197static int intro_done;
2198
2199static float Phase(float t, float a, float b) { return t <= a ? 0 : t >= b ? 1 : (t - a) / (b - a); }
2200static float Ease_Out(float t) { return 1 - (1 - t) * (1 - t) * (1 - t); }
2201
2202static void Intro_Hurry(void)
2203{
2204 if (intro_t0 && t_now - intro_t0 < INTRO_FADE_US) intro_t0 = t_now - INTRO_FADE_US;
2205}
2206
2207static void Draw_Intro(void)
2208{
2209 uint64_t now = plat_time_us();
2210 if (!intro_t0) intro_t0 = now;
2211 if (now - intro_t0 >= INTRO_US) {
2212 intro_done = 1;
2213 image_free(logo_big);
2214 logo_big = NULL;
2215 dirty = 1;
2216 return;
2217 }
2218 float t = (now - intro_t0) / 1e6f, out = 1 - Phase(t, INTRO_FADE_US / 1e6f, INTRO_US / 1e6f);
2219 // the backdrop over the app, from black
2220 if (backdrop) gfx_image(backdrop, 0, 0, SCREEN_W, SCREEN_H, 0, (int)(255 * out));
2221 else gfx_rect(0, 0, SCREEN_W, SCREEN_H, with_alpha(C_BG, (int)(255 * out)));
2222 float in = Phase(t, 0, 0.45f);
2223 if (in < 1) gfx_rect(0, 0, SCREEN_W, SCREEN_H, with_alpha(0xff000000, (int)(255 * (1 - in) * out)));
2224 // a glow passing behind the logo, left to right
2225 float g = Phase(t, 0.7f, 1.6f);
2226 if (g > 0 && g < 1) {
2227 int gx = SCREEN_W / 2 - 420 + (int)(840 * Ease_Out(g)), ga = (int)(4 * g * (1 - g) * 5 * out + 0.5f);
2228 for (int r = 110; r >= 6; r -= 4) gfx_circle(gx, INTRO_Y, r, with_alpha(C_CYAN, ga)); // (many faint ones: soft, no rings)
2229 }
2230 // the logo, rising a little as it grows to its size and comes in
2231 float e = Ease_Out(Phase(t, 0.25f, 1.0f));
2232 int lw = (int)(INTRO_LOGO_W * (0.86f + 0.14f * e)), lh = lw * INTRO_LOGO_H / INTRO_LOGO_W;
2233 const image_t *im = logo_big ? logo_big : logo;
2234 if (im) gfx_image(im, (SCREEN_W - lw) / 2, INTRO_Y - lh / 2 + (int)(14 * (1 - e)), lw, lh, 0, (int)(255 * e * out));
2235 // a line drawn out from the middle under it, then the address
2236 int half = (int)(150 * Ease_Out(Phase(t, 0.85f, 1.35f)));
2237 if (half > 0) gfx_rect(SCREEN_W / 2 - half, INTRO_Y + 52, half * 2, 2, with_alpha(C_CYAN, (int)(220 * out)));
2238 float a = Phase(t, 1.05f, 1.45f);
2239 if (a > 0) font_draw_center(FONT_MEDIUM, 20, SCREEN_W / 2, INTRO_Y + 68, "ps3.world", with_alpha(C_TEXT2, (int)(255 * a * out)), 0);
2240 dirty = 1; // (the next frame of it)
2241}
2242
2243void ui_update(void)
2244{
2245 t_now = plat_time_us();
2246 input_update();
2247 Fetch_Tick();
2248 Files_Tick();
2249 Icons_Tick();
2250 Download_Tick();
2251 Pack_Tick();
2252 Surprise_Tick();
2253 Desc_Tick();
2254 if (screen == SCR_HOME) {
2255 Ticker_Build();
2256 if (Ticker_Scrolls() && t_now - ticker_at >= (gfx_gpu() ? 33000u : 66000u)) dirty = 1;
2257 }
2258 if ((screen == SCR_HOME || screen == SCR_DOWNLOADS) && (!free_checked || t_now - free_checked > 10000000)) {
2259 long long f = plat_free_bytes("/dev_hdd0");
2260 free_checked = t_now;
2261 if (f != free_bytes) { free_bytes = f; dirty = 1; }
2262 }
2263
2264 if (searching) {
2265 char q[128];
2266 int r = plat_osk_poll(q, sizeof(q));
2267 if (r) {
2268 searching = 0;
2269 view_t *v = Cur_View();
2270 if (r > 0 && v && q[0]) {
2271 snprintf(v->query, sizeof(v->query), "%s", q);
2272 Search_Run(v, Cur_List());
2273 }
2274 dirty = 1;
2275 }
2276 return;
2277 }
2278 view_t *v = Cur_View();
2279 if (v && v->search_wait) Search_Run(v, Cur_List()); // (once the search.txt is here)
2280
2281 uint32_t p = input_pressed();
2282 if (!intro_done) {
2283 if (p) Intro_Hurry();
2284 return;
2285 }
2286 // right to left (Arabic): the screen is mirrored, so are the directions (Right moves right on it)
2287 if (lang_current() == LANG_AR) {
2288 uint32_t sw = p & ~(BTN_LEFT | BTN_RIGHT | BTN_L1 | BTN_R1);
2289 if (p & BTN_LEFT) sw |= BTN_RIGHT;
2290 if (p & BTN_RIGHT) sw |= BTN_LEFT;
2291 if (p & BTN_L1) sw |= BTN_R1;
2292 if (p & BTN_R1) sw |= BTN_L1;
2293 p = sw;
2294 }
2295 // a tick for a move, a blip for Cross, a lower one for Circle
2296 if (p & (BTN_UP | BTN_DOWN | BTN_LEFT | BTN_RIGHT | BTN_L1 | BTN_R1 | BTN_L2 | BTN_R2)) plat_sound(SND_MOVE);
2297 if (p & BTN_CROSS) plat_sound(SND_OK);
2298 else if (p & BTN_CIRCLE) plat_sound(SND_BACK);
2299 switch (screen) {
2300 case SCR_START:
2301 if ((p & BTN_CROSS) && start_err) Fetch_Start();
2302 break;
2303 case SCR_HOME: Input_Home(p); break;
2304 case SCR_LIST:
2305 if (Picking()) Input_Picker(p);
2306 else Input_List(p);
2307 break;
2308 case SCR_DETAIL: Input_Detail(p); break;
2309 case SCR_PROFILE: Input_Profile(p); break;
2310 case SCR_DOWNLOADS: Input_Downloads(p); break;
2311 case SCR_LANG: Input_Lang(p); break;
2312 case SCR_CREDITS: Input_Credits(p); break;
2313 case SCR_NEWS: Input_News(p); break;
2314 }
2315}
2316
2317int ui_needs_redraw(void)
2318{
2319 int d = dirty;
2320 dirty = 0;
2321 return d;
2322}
2323
2324void ui_force_redraw(void)
2325{
2326 dirty = 1;
2327}
2328
2329// the sections, as tabs (what L1/R1 change on the categories screen)
2330static int Section_Tabs(int x, int y, int maxx)
2331{
2332 const char *labels[NUM_TYPES];
2333 for (int t = 0; t < NUM_TYPES; t++) labels[t] = tr(section_names[t]);
2334 return Tabs(x, y, maxx, labels, NUM_TYPES, cur);
2335}
2336
2337static void Draw_Header(void)
2338{
2339 if (logo) gfx_image(logo, LIST_X, 40, 220, 31, 0, 255);
2340 const view_t *v = Cur_View();
2341 char line[200], n[16];
2342 uint32_t c = C_DIM;
2343 int px = 18;
2344 // on the right: the search, a message, or the section and how much is in it (in a pack, its name)
2345 if (v && (v->query[0] || v->search_wait)) {
2346 if (v->search_wait) snprintf(line, sizeof(line), tr("Searching for \"%s\"..."), v->query);
2347 else snprintf(line, sizeof(line), tr("Search: \"%s\" (%d%s)"), v->query, v->num_results, v->num_results == MAX_RESULTS ? "+" : "");
2348 c = C_WARN;
2349 } else if (banner[0]) {
2350 snprintf(line, sizeof(line), "%s", banner);
2351 c = C_WARN;
2352 } else if (folder.pages) {
2353 snprintf(line, sizeof(line), "%s", folder.name);
2354 c = C_TEXT;
2355 } else if (v && v->sort) {
2356 snprintf(line, sizeof(line), "%s", tr(v->sort == SORT_NEWEST ? "Newest first" : "Largest first"));
2357 c = C_CYAN;
2358 } else if (recent_mode && Cur_List()) {
2359 Fmt_Count(Cur_List()->count, n, sizeof(n));
2360 snprintf(line, sizeof(line), "%s %s", tr("Recently added"), n);
2361 c = C_CYAN;
2362 } else {
2363 Fmt_Count(ix->count[cur], n, sizeof(n));
2364 snprintf(line, sizeof(line), "%s %s", tr(section_names[cur]), n);
2365 }
2366 int rw = font_width(FONT_MEDIUM, px, line);
2367 if (rw > 420) rw = 420;
2368 font_draw(FONT_MEDIUM, px, SCREEN_W - LIST_X - rw, 96, line, c, rw);
2369 // L1/R1: the section's categories (the sections, if it has only the one list)
2370 int nl = Cats(cur), maxx = SCREEN_W - LIST_X - rw - 24;
2371 if (nl > 1 && !recent_mode) {
2372 const char *labels[32];
2373 for (int i = 0; i < nl && i < 32; i++) labels[i] = tr(ix->lists[cur][Cat_List(cur, i)].name);
2374 Tabs(LIST_X, 87, maxx, labels, nl < 32 ? nl : 32, List_Cat(cur, cur_list[cur]));
2375 } else
2376 Section_Tabs(LIST_X, 87, maxx);
2377}
2378
2379static void Badge(int *rx, int y, const char *text, uint32_t c)
2380{
2381 int w = font_width(FONT_MEDIUM, 15, text) + 16;
2382 *rx -= w;
2383 gfx_round_rect(*rx, y, w, 24, 12, with_alpha(c, 50));
2384 font_draw(FONT_MEDIUM, 15, *rx + 8, y + 3, text, c, 0);
2385 *rx -= 8;
2386}
2387
2388// the logo, where we are (as the site's crumbs), and the catalogue's state, over the cards
2389#define TOP_TABS_MAX (SCREEN_W - LIST_X - 170) // (the version is drawn top right)
2390static void Draw_Top(const char *crumb)
2391{
2392 if (logo) gfx_image(logo, LIST_X, 40, 220, 31, 0, 255);
2393 if (crumb) {
2394 int w = font_width(FONT_BOLD, 18, crumb) + 28;
2395 gfx_round_rect(LIST_X + 236, 38, w, 34, 17, 0x404ad9ff);
2396 gfx_round_rect(LIST_X + 237, 39, w - 2, 32, 16, 0xff0a1622);
2397 font_draw(FONT_BOLD, 18, LIST_X + 250, 44, crumb, C_CYAN, 0);
2398 }
2399 if (banner[0]) font_draw_right(FONT_REGULAR, 18, SCREEN_W - LIST_X, 68, banner, C_WARN);
2400 else if (fetch_job && !(screen == SCR_HOME && dl)) font_draw_right(FONT_REGULAR, 18, SCREEN_W - LIST_X, 68, tr("Checking for updates..."), C_DIM);
2401}
2402
2403// the download going on, bottom right (where the list's position goes otherwise): 1 if drawn
2404static int Draw_Strip_At(int y)
2405{
2406 if (!dl) return 0;
2407 int right = SCREEN_W - LIST_X, bw = 120, more = Queue_Busy() - 1;
2408 float frac = dl->size ? (float)dl->got / (float)dl->size : 0;
2409 if (frac > 1) frac = 1;
2410 gfx_round_rect(right - bw, y + 9, bw, 8, 4, 0x40ffffff);
2411 gfx_round_rect(right - bw, y + 9, (int)(bw * frac), 8, 4, C_ACCENT);
2412 char name[1][256], line[320];
2413 Wrap(FONT_MEDIUM, 19, queue[dl_q].it->title, 180, name, 1);
2414 if (more > 0) snprintf(line, sizeof(line), tr("%s %d%% · %d more"), name[0], (int)(frac * 100), more);
2415 else snprintf(line, sizeof(line), "%s %d%%", name[0], (int)(frac * 100));
2416 if (dl->state == DL_RUNNING && dl_speed > 0) { // and how fast it's coming
2417 char spd[32];
2418 size_t k = strlen(line);
2419 Fmt_Size((long long)dl_speed, spd, sizeof(spd));
2420 snprintf(line + k, sizeof(line) - k, " \xc2\xb7 %s/s", spd);
2421 }
2422 font_draw_right(FONT_MEDIUM, 19, right - bw - 14, y, line, C_TEXT);
2423 return 1;
2424}
2425
2426static int Draw_Strip(void) { return Draw_Strip_At(SCREEN_H - 55); }
2427
2428// "3 Nov 2026", or "" when it isn't known
2429static void Fmt_Date(long long t, char *out, int size)
2430{
2431 time_t tt = (time_t)t;
2432 struct tm *tm = t > 0 ? localtime(&tt) : NULL;
2433 out[0] = 0;
2434 if (tm) strftime(out, size, "%d %b %Y", tm);
2435}
2436
2437// "212 GB free on the HDD · 14 downloaded"
2438static void Space_Line(char *out, int size)
2439{
2440 char fr[32];
2441 int n = store_num_downloads();
2442 Fmt_Size(free_bytes, fr, sizeof(fr));
2443 if (free_bytes >= 0) snprintf(out, size, tr("%s free on the HDD · %d downloaded"), fr, n);
2444 else snprintf(out, size, tr("%d downloaded"), n);
2445}
2446
2447// a list's count: an avatar list's rows are packs
2448static void Count_Text(int t, int n, char *out, int size)
2449{
2450 char num[16];
2451 Fmt_Count(n, num, sizeof(num));
2452 const char *f = t == TYPE_THEME ? (n == 1 ? "%s theme" : "%s themes") : t == TYPE_AVATAR ? (n == 1 ? "%s pack" : "%s packs") :
2453 (n == 1 ? "%s item" : "%s items");
2454 snprintf(out, size, tr(f), num);
2455}
2456
2457/*
2458==================
2459The New ticker: the titles in each section's Recently added list, scrolling along the top
2460of the home screen (from the lists' search.txt: a title a line, asked for as needed)
2461==================
2462*/
2463#define TICKER_SPEED 70 // canvas pixels a second
2464#define TICKER_GAP 80 // between the end and the start again
2465#define TICKER_PER 12 // titles from each section at most
2466#define TICKER_SEGS (NUM_TYPES * (TICKER_PER + 1))
2467#define TICKER_DOT 34 // the space a dot between titles takes
2468
2469// in pieces (a section's name, then its titles): the font draws a line of a few hundred characters at most
2470typedef struct {
2471 char text[128];
2472 int w, head; // head: a section's name (cyan, after a wider gap)
2473} tick_t;
2474static tick_t ticks[TICKER_SEGS];
2475static int num_ticks, ticker_w; // ticker_w 0: nothing new
2476
2477static void Ticker_Build(void)
2478{
2479 tick_t t2[TICKER_SEGS];
2480 int n = 0;
2481 for (int t = 0; ix && t < NUM_TYPES; t++)
2482 for (int l = 0; l < ix->num_lists[t]; l++) {
2483 const cat_list_t *list = &ix->lists[t][l];
2484 if (strcmp(list->name, "Recently added") || !list->count) continue;
2485 file_slot_t *fs = File_Get(&list->search, 1);
2486 if (!fs || (!fs->lines && !fs->failed)) return; // (on its way: built when it's here)
2487 if (!fs->lines) continue;
2488 snprintf(t2[n].text, sizeof(t2[n].text), "%s:", tr(section_names[t]));
2489 t2[n++].head = 1;
2490 for (int i = 0; i < fs->lines->count && i < TICKER_PER; i++) {
2491 const char *line = fs->lines->line[i], *tab = memchr(line, '\t', fs->lines->len[i]);
2492 int len = tab ? (int)(tab - line) : fs->lines->len[i];
2493 snprintf(t2[n].text, sizeof(t2[n].text), "%.*s", len, line);
2494 t2[n++].head = 0;
2495 }
2496 }
2497 // the same as shown already: nothing to do (the widths are measured only when it changes)
2498 int same = n == num_ticks;
2499 for (int i = 0; same && i < n; i++) same = t2[i].head == ticks[i].head && !strcmp(t2[i].text, ticks[i].text);
2500 if (same) return;
2501 ticker_w = 0;
2502 for (int i = 0; i < n; i++) {
2503 ticks[i] = t2[i];
2504 ticks[i].w = font_width(ticks[i].head ? FONT_BOLD : FONT_MEDIUM, 19, ticks[i].text);
2505 ticker_w += ticks[i].w + (i == 0 ? 0 : ticks[i].head ? TICKER_DOT * 2 : TICKER_DOT);
2506 }
2507 num_ticks = n;
2508 dirty = 1;
2509}
2510
2511// the strip it scrolls in: right of the logo, short of the version
2512#define TICKER_X (LIST_X + 236)
2513#define TICKER_W (SCREEN_W - LIST_X - 170 - TICKER_X)
2514
2515static int Ticker_Scrolls(void)
2516{
2517 return ticker_w && ticker_w > TICKER_W - 80;
2518}
2519
2520// the pieces from x (only what falls between x0 and x1 is drawn)
2521static void Ticker_Run(int x, int y, int x0, int x1)
2522{
2523 for (int i = 0; i < num_ticks && x < x1; i++) {
2524 if (i) {
2525 int gap = ticks[i].head ? TICKER_DOT * 2 : TICKER_DOT;
2526 if (!ticks[i].head && x + gap > x0) font_draw_center(FONT_MEDIUM, 19, x + gap / 2, y, "\xc2\xb7", C_DIM, 0);
2527 x += gap;
2528 }
2529 if (x + ticks[i].w > x0)
2530 font_draw(ticks[i].head ? FONT_BOLD : FONT_MEDIUM, 19, x, y, ticks[i].text, ticks[i].head ? C_CYAN : C_TEXT2, 0);
2531 x += ticks[i].w;
2532 }
2533}
2534
2535static void Draw_Ticker(void)
2536{
2537 if (!ticker_w) return;
2538 const char *label = tr("New");
2539 int lw = font_width(FONT_BOLD, 16, label) + 22, y = 40;
2540 gfx_round_rect(TICKER_X, y, lw, 28, 14, C_CYAN);
2541 font_draw(FONT_BOLD, 16, TICKER_X + 11, y + 6, label, 0xff05070d, 0);
2542 int x0 = TICKER_X + lw + 14, x1 = TICKER_X + TICKER_W;
2543 ticker_at = t_now;
2544 gfx_clip(x0, y, x1 - x0, 28);
2545 if (!Ticker_Scrolls()) Ticker_Run(x0, y + 5, x0, x1);
2546 else {
2547 int loop = ticker_w + TICKER_GAP;
2548 int off = (int)((t_now / 1000) * TICKER_SPEED / 1000 % (uint64_t)loop);
2549 Ticker_Run(x0 - off, y + 5, x0, x1);
2550 Ticker_Run(x0 - off + loop, y + 5, x0, x1);
2551 }
2552 gfx_unclip();
2553}
2554
2555// the home screen's Favourites card: how many, and the first of them
2556static void Draw_Fav_Card(int x, int y, int w, int h, int on)
2557{
2558 int n = favs ? favs->count : 0;
2559 char line[200], lines[2][256];
2560 Card_Frame(x, y, w, h, on);
2561 snprintf(line, sizeof(line), "\xe2\x98\x85 %s", tr("Favourites"));
2562 font_draw(FONT_BOLD, 30, x + 28, y + 20, line, n ? C_TEXT : C_DIM, 320);
2563 if (!n) { // (two lines, smaller: the card's half the screen)
2564 int k = Wrap(FONT_REGULAR, 17, tr("No favourites yet. Press Select on an item to add it."), w - 56, lines, 2);
2565 for (int i = 0; i < k; i++) font_draw(FONT_REGULAR, 17, x + 28, y + 58 + i * 22, lines[i], C_TEXT2, 0);
2566 return;
2567 }
2568 snprintf(line, sizeof(line), tr(n == 1 ? "%d favourite" : "%d favourites"), n);
2569 font_draw(FONT_REGULAR, 19, x + 28, y + 62, line, C_TEXT2, 300);
2570 // the first few, as their pictures
2571 int px = x + 360, py = y + (h - THUMB_H) / 2;
2572 for (int i = 0; i < n && px + THUMB_W <= x + w - 28; i++) {
2573 const cat_item_t *it = &favs->items[i];
2574 if (it->type == TYPE_THEME) { Thumb_Draw(it, it->title, px, py); px += THUMB_W + 12; }
2575 else { Icon_Draw(it, px, py, THUMB_H); px += THUMB_H + 12; }
2576 }
2577}
2578
2579// the home screen's Recently added card: a New pill, and how many in each section
2580static void Draw_Recent_Card(int x, int y, int w, int h, int on)
2581{
2582 char line[200], num[16];
2583 Card_Frame(x, y, w, h, on);
2584 const char *label = tr("New");
2585 int pw = font_width(FONT_BOLD, 16, label) + 22;
2586 gfx_round_rect(x + 28, y + 22, pw, 28, 14, C_CYAN);
2587 font_draw(FONT_BOLD, 16, x + 39, y + 28, label, 0xff05070d, 0);
2588 font_draw(FONT_BOLD, 30, x + 28 + pw + 14, y + 20, tr("Recently added"), C_TEXT, w - 56 - pw - 14);
2589 line[0] = 0;
2590 for (int t = 0; t < NUM_TYPES; t++) {
2591 const cat_list_t *l = List(t, Recent_Of(t));
2592 if (!l) continue;
2593 Fmt_Count(l->count, num, sizeof(num));
2594 size_t k = strlen(line);
2595 snprintf(line + k, sizeof(line) - k, "%s%s %s", k ? " \xc2\xb7 " : "", tr(section_names[t]), num);
2596 }
2597 font_draw(FONT_REGULAR, 19, x + 28, y + 62, line[0] ? line : tr("Nothing here yet."), C_TEXT2, w - 56);
2598}
2599
2600static void Draw_Home(void)
2601{
2602 Draw_Top(NULL);
2603 Draw_Ticker();
2604 int rw = HOME_RW, lw = LIST_W - rw - CARD_GAP, rh = (HOME_H - CARD_GAP) / 2;
2605 for (int t = 0; t < NUM_TYPES; t++) {
2606 const card_text_t *c = &home_cards[t];
2607 const char *chips[16];
2608 int n = 0;
2609 for (int l = 0; l < ix->num_lists[t] && n < 16; l++) // its categories ("All ..." is the whole section)
2610 if (strncmp(ix->lists[t][l].name, "All ", 4) && l != Recent_Of(t)) chips[n++] = ix->lists[t][l].name;
2611 if (t == TYPE_HOMEBREW)
2612 Card(LIST_X, CARD_Y, lw, HOME_H, home_sel == t, home_art[t], 0, c->kicker, c->title, 64, c->blurb, chips, n);
2613 else
2614 Card(LIST_X + lw + CARD_GAP, CARD_Y + (t - 1) * (rh + CARD_GAP), rw, rh, home_sel == t, home_art[t], 0,
2615 c->kicker, c->title, 44, c->blurb, chips, n);
2616 }
2617 Draw_Fav_Card(LIST_X, CARD_Y + HOME_H + CARD_GAP, lw, FAV_H, home_sel == HOME_FAVS);
2618 Draw_Recent_Card(LIST_X + lw + CARD_GAP, CARD_Y + HOME_H + CARD_GAP, rw, FAV_H, home_sel == HOME_RECENT);
2619 // top right, under the version: a note (Draw_Top's), else the download, a new version, the check, or the free space
2620 if (!banner[0] && !Draw_Strip_At(66)) {
2621 char line[128];
2622 if (Update_Available()) {
2623 snprintf(line, sizeof(line), tr("PS3 World %s is out: press Square to get it"), ix->app.version);
2624 font_draw_right(FONT_MEDIUM, 19, SCREEN_W - LIST_X, 67, line, C_CYAN);
2625 } else if (!fetch_job) {
2626 Space_Line(line, sizeof(line));
2627 font_draw_right(FONT_MEDIUM, 18, SCREEN_W - LIST_X, 68, line, C_DIM);
2628 }
2629 }
2630 // (the bottom row is the hints' alone: Quit made it too long for the free space beside it)
2631 int x = Hint(LIST_X, G_CROSS, tr("Open"));
2632 x = Hint(x, G_TRIANGLE, tr("My downloads"));
2633 if (Update_Available()) x = Hint(x, G_SQUARE, tr("Update"));
2634 x = Hint(x, G_SELECT, tr("Language"));
2635 x = Hint(x, G_START, tr("Credits & contact"));
2636 Hint(x, G_CIRCLE, tr("Quit"));
2637}
2638
2639static void Draw_Picker(void)
2640{
2641 Draw_Top(NULL);
2642 Section_Tabs(LIST_X + 236, 38, TOP_TABS_MAX);
2643 int n = Cats(cur), cols = Picker_Cols();
2644 char count[48];
2645 if (cur == TYPE_HOMEBREW) {
2646 int rows = (n + cols - 1) / cols, h = (CARD_H - CARD_GAP * (rows - 1)) / rows;
2647 for (int i = 0; i < n; i++) {
2648 const cat_list_t *l = &ix->lists[cur][Cat_List(cur, i)];
2649 image_t *art;
2650 const card_text_t *c = Cat_Card(l->name, &art);
2651 const char *chips[1] = {count};
2652 int in_row = i / cols < rows - 1 ? cols : n - (rows - 1) * cols; // (a shorter last row's cards widen to fill it)
2653 int w = (LIST_W - CARD_GAP * (in_row - 1)) / in_row;
2654 Count_Text(cur, l->count, count, sizeof(count));
2655 Card(LIST_X + (i % cols) * (w + CARD_GAP), CARD_Y + (i / cols) * (h + CARD_GAP), w, h, i == picker_sel, art, CAT_W,
2656 c ? c->kicker : NULL, c ? c->title : l->name, 40, c ? c->blurb : NULL, chips, 1);
2657 }
2658 } else {
2659 int w = (LIST_W - CARD_GAP * (cols - 1)) / cols, shown = Picker_Rows_Shown();
2660 for (int i = picker_top * cols; i < n && i < (picker_top + shown) * cols; i++) {
2661 const cat_list_t *l = &ix->lists[cur][Cat_List(cur, i)];
2662 int r = i / cols - picker_top;
2663 Count_Text(cur, l->count, count, sizeof(count));
2664 Tile(LIST_X + (i % cols) * (w + CARD_GAP), CARD_Y + r * (TILE_H + CARD_GAP), w, TILE_H, i == picker_sel, l->name, count);
2665 }
2666 int rows = (n + cols - 1) / cols;
2667 if (rows > shown) { // where in the grid
2668 int h = shown * (TILE_H + CARD_GAP) - CARD_GAP, bh = h * shown / rows;
2669 int by = CARD_Y + (h - bh) * picker_top / (rows - shown);
2670 gfx_round_rect(LIST_X + LIST_W + 8, by, 4, bh, 2, 0x99ffffff);
2671 }
2672 }
2673 int x = Hint(LIST_X, G_CROSS, tr("Open"));
2674 x = Hint(x, G_SQUARE, tr("Surprise me"));
2675 Hint(x, G_CIRCLE, tr("Back"));
2676 if (!Draw_Strip() && cur == TYPE_THEME)
2677 font_draw_right(FONT_REGULAR, 18, SCREEN_W - LIST_X, SCREEN_H - 55,
2678 tr("Not every theme has a preview: some were archived without one."), C_DIM);
2679}
2680
2681// homebrew installed on this PS3: when its /dev_hdd0/game/<title ID>/PARAM.SFO was written (as the PKG
2682// installed; an empty folder isn't an install), 0 if it isn't. Asked a folder at a time and remembered for
2683// a minute (installing is done in the XMB, while the app isn't running, mostly)
2684#define INST_SLOTS 64
2685static struct { char tid[10]; long long when; uint64_t at; } inst[INST_SLOTS];
2686static int inst_next;
2687
2688static long long Install_Time(const cat_item_t *it)
2689{
2690 const char *tid = it->title_id;
2691 if (it->type != TYPE_HOMEBREW || strlen(tid) != 9 || strspn(tid, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") != 9) return 0;
2692 for (int i = 0; i < INST_SLOTS; i++)
2693 if (!strcmp(inst[i].tid, tid) && t_now - inst[i].at < 60000000ULL) return inst[i].when;
2694 char path[64];
2695 snprintf(path, sizeof(path), "/dev_hdd0/game/%s/PARAM.SFO", tid);
2696 int k = inst_next++ % INST_SLOTS;
2697 snprintf(inst[k].tid, sizeof(inst[k].tid), "%s", tid);
2698 inst[k].when = plat_file_mtime(path);
2699 inst[k].at = t_now;
2700 return inst[k].when;
2701}
2702static int Installed(const cat_item_t *it) { return Install_Time(it) > 0; }
2703
2704// a newer version than the one here: installed before the catalogue's PKG last changed, or (not installed)
2705// downloaded with the app as a file the catalogue has since replaced
2706#define CLOCK_SANE 1262304000LL // 2010: an install time before that is a PS3 clock that was never set
2707static int Update_Due(const cat_item_t *it)
2708{
2709 if (it->type != TYPE_HOMEBREW) return 0;
2710 long long t = Install_Time(it);
2711 if (t > 0) return it->updated > 0 && t >= CLOCK_SANE && t < it->updated;
2712 store_rec_t r;
2713 for (int i = 0; store_download_at(i, &r); i++)
2714 if (!strcmp(r.id, it->id)) return Dls_Newer(&r);
2715 return 0;
2716}
2717
2718// an item's title and the line under it (author, version, category; a pack's count and regions) from tx,
2719// its size and badges right-aligned to rx
2720static void Item_Lines(const cat_item_t *it, int tx, int y, int rx)
2721{
2722 char size[32], sub[300];
2723 Fmt_Size(it->size, size, sizeof(size));
2724 rx -= font_draw_right(FONT_MEDIUM, 19, rx, y + 14, size, C_DIM) + 14;
2725 if (it->list[0]) // a pack: how many avatars, and its regions
2726 snprintf(sub, sizeof(sub), "%s%s%s", it->description, it->group[0] ? " \xc2\xb7 " : "", it->group);
2727 else {
2728 if (Update_Due(it)) Badge(&rx, y + 12, tr("Update"), C_ACCENT); // (a newer version than the one here)
2729 else if (Installed(it)) Badge(&rx, y + 12, tr("Installed"), C_OK); // (rather than Downloaded: it's further along)
2730 else if (store_downloaded(it)) Badge(&rx, y + 12, tr("Downloaded"), C_OK);
2731 if (Fav_Find(it->id) >= 0) Badge(&rx, y + 12, "\xe2\x98\x85", C_WARN); // a star
2732 if (it->requires == REQ_CFW) Badge(&rx, y + 12, tr("CFW only"), C_WARN); // (HEN or CFW goes without saying: the app needs one)
2733 snprintf(sub, sizeof(sub), "%s%s%s%s%s", it->author[0] ? it->author : tr("Unknown author"),
2734 it->version[0] ? " \xc2\xb7 v" : "", it->version, it->group[0] && it->type != TYPE_HOMEBREW ? " \xc2\xb7 " : "",
2735 it->type != TYPE_HOMEBREW ? tr(it->group) : "");
2736 }
2737 font_draw(FONT_MEDIUM, 22, tx, y, it->title, C_TEXT, rx - tx - 10);
2738 font_draw(FONT_REGULAR, 17, tx, y + 29, sub, C_DIM, rx - tx - 10);
2739}
2740
2741// the title at a position from the list's search file, before its page is here ("" if that isn't either)
2742static void Search_Title(const cat_list_t *l, int pos, char *title, int size)
2743{
2744 int len = 0;
2745 const char *line = Search_Line(l, pos, &len);
2746 title[0] = 0;
2747 if (line) {
2748 const char *tab = memchr(line, '\t', len);
2749 snprintf(title, size, "%.*s", (int)(tab ? tab - line : len), line);
2750 }
2751}
2752
2753// themes and avatars: their pictures, a ring round the one picked; under them, what it is
2754static void Draw_Grid(const cat_list_t *l, const view_t *v, int count)
2755{
2756 int cols = Grid_Cols(cur), w = Grid_W(cur), h = Grid_H(cur);
2757 char title[200];
2758 for (int k = 0; k < cols * GRID_ROWS && v->top + k < count; k++) {
2759 int row = v->top + k, pos = View_Pos(v, row);
2760 int x = LIST_X + (k % cols) * (w + CARD_GAP), y = LIST_Y + (k / cols) * (h + GRID_GAP);
2761 file_slot_t *slot;
2762 const cat_item_t *it = Item_At(l, pos, &slot);
2763 if (row == v->sel) gfx_round_rect(x - 5, y - 5, w + 10, h + 10, 15, C_ACCENT);
2764 image_t *img = it ? Art_Get(it, it->preview[0] != 0, w, h, NULL) : NULL; // (the bigger picture when there is one)
2765 if (img) gfx_image(img, x, y, w, h, 10, 255);
2766 else { // (its page or its picture isn't here yet: the title's first letter)
2767 if (it) snprintf(title, sizeof(title), "%s", it->title);
2768 else Search_Title(l, pos, title, sizeof(title));
2769 gfx_round_rect(x, y, w, h, 10, 0xff1a1c20);
2770 char ch[2] = {Letter_Of(title[0] ? title : "?"), 0};
2771 font_draw_center(FONT_BOLD, h / 3, x + w / 2, y + h / 3, ch, 0xff3a4152, 0);
2772 }
2773 if (!it) continue;
2774 // downloaded, a favourite: small marks in the corner; a pack: how many in it
2775 int bx = x + w - 8;
2776 if (!it->list[0] && store_downloaded(it)) { gfx_circle(bx - 9, y + 17, 11, C_OK); font_draw_center(FONT_BOLD, 14, bx - 9, y + 9, "\xe2\x9c\x93", 0xff05070d, 0); bx -= 26; }
2777 if (!it->list[0] && Fav_Find(it->id) >= 0) { gfx_circle(bx - 9, y + 17, 11, C_WARN); font_draw_center(FONT_BOLD, 14, bx - 9, y + 9, "\xe2\x98\x85", 0xff05070d, 0); }
2778 }
2779 // the one picked: its title, what it is, size and badges
2780 int pos = View_Pos(v, v->sel), ty = LIST_Y + ROWS * ROW_H - 42;
2781 file_slot_t *slot;
2782 const cat_item_t *it = count ? Item_At(l, pos, &slot) : NULL;
2783 gfx_rect(LIST_X, ty - 9, LIST_W, 1, 0x22ffffff);
2784 if (it) Item_Lines(it, LIST_X, ty, LIST_X + LIST_W - 4);
2785 else if (count) {
2786 Search_Title(l, pos, title, sizeof(title));
2787 if (title[0]) font_draw(FONT_MEDIUM, 22, LIST_X, ty, title, C_TEXT, LIST_W);
2788 font_draw(FONT_REGULAR, 17, LIST_X, ty + 29, slot && slot->failed ? Error_Text(slot->err_kind, slot->err) : tr("Loading..."),
2789 slot && slot->failed ? C_ERR : C_DIM, LIST_W);
2790 }
2791 // where in it
2792 int rows = (count + cols - 1) / cols;
2793 if (rows > GRID_ROWS) {
2794 int gh = GRID_ROWS * (h + GRID_GAP) - GRID_GAP, bh = gh * GRID_ROWS / rows;
2795 if (bh < 24) bh = 24;
2796 int by = LIST_Y + (int)((long long)(gh - bh) * (v->top / cols) / (rows - GRID_ROWS));
2797 gfx_round_rect(LIST_X + LIST_W + 6, by, 4, bh, 2, 0x99ffffff);
2798 }
2799}
2800
2801static void Draw_List(void)
2802{
2803 cat_list_t *l = Cur_List();
2804 view_t *v = Cur_View();
2805 Draw_Header();
2806 gfx_round_rect(LIST_X - 12, LIST_Y - 8, LIST_W + 24, ROWS * ROW_H + 16, 14, C_PANEL);
2807 if (!l || !v) { // a section with no lists (none in the catalogue, or none passed)
2808 font_draw_center(FONT_MEDIUM, 22, SCREEN_W / 2, LIST_Y + 180, tr("Nothing here yet."), C_DIM, SCREEN_W - 200);
2809 Hint(LIST_X, G_CIRCLE, tr("Back"));
2810 return;
2811 }
2812 int count = View_Count(v, l);
2813 Clamp(v, count); // (every path in: a section's view is never drawn out of range)
2814 if (!count && !v->search_wait)
2815 font_draw_center(FONT_MEDIUM, 22, SCREEN_W / 2, LIST_Y + 180,
2816 !v->query[0] ? tr("Nothing here yet.") : search_banner ? banner :
2817 tr("Nothing matches the search. Press Square to clear it."), C_DIM, SCREEN_W - 200);
2818 if (Gridded(cur)) Draw_Grid(l, v, count);
2819 for (int r = 0; !Gridded(cur) && r < ROWS && v->top + r < count; r++) {
2820 int row = v->top + r, pos = View_Pos(v, row);
2821 int y = LIST_Y + r * ROW_H;
2822 file_slot_t *slot;
2823 const cat_item_t *it = Item_At(l, pos, &slot);
2824 if (row == v->sel) gfx_round_rect(LIST_X - 4, y, LIST_W + 8, ROW_H - 4, 10, C_ROW_ON);
2825 int thumbs = cur == TYPE_THEME; // (themes: a 16:9 preview, not a square icon)
2826 int pw = thumbs ? THUMB_W : ICON_LIST, ph = thumbs ? THUMB_H : ICON_LIST;
2827 int tx = LIST_X + 6 + pw + 16, iy = y + (ROW_H - 4 - ph) / 2;
2828 if (!it) {
2829 // its page isn't here yet: the title from the search file if we have it
2830 char title[200];
2831 Search_Title(l, pos, title, sizeof(title));
2832 if (thumbs) Thumb_Draw(NULL, title[0] ? title : "?", LIST_X + 6, iy);
2833 else Placeholder(title[0] ? title : "?", LIST_X + 6, iy, ICON_LIST);
2834 if (title[0]) font_draw(FONT_MEDIUM, 22, tx, y + 8, title, C_TEXT, LIST_W - tx);
2835 const char *state = slot && slot->failed ? Error_Text(slot->err_kind, slot->err) : tr("Loading...");
2836 font_draw(FONT_REGULAR, 17, tx, title[0] ? y + 37 : y + 22, state, slot && slot->failed ? C_ERR : C_DIM, LIST_X + LIST_W - tx - 12);
2837 continue;
2838 }
2839 if (thumbs) Thumb_Draw(it, it->title, LIST_X + 6, iy);
2840 else Icon_Draw(it, LIST_X + 6, iy, ICON_LIST);
2841 Item_Lines(it, tx, y + 8, LIST_X + LIST_W - 12);
2842 }
2843 // where in the list, and the letter
2844 if (!Gridded(cur) && count > ROWS) {
2845 int h = ROWS * ROW_H, bh = h * ROWS / count;
2846 if (bh < 24) bh = 24;
2847 int by = LIST_Y + (int)((long long)(h - bh) * v->sel / (count - 1));
2848 gfx_round_rect(LIST_X + LIST_W + 2, by, 4, bh, 2, 0x99ffffff);
2849 }
2850 int x = LIST_X;
2851 x = Hint(x, G_CROSS, tr("Open"));
2852 x = Hint(x, G_TRIANGLE, tr("Search"));
2853 if (cur == TYPE_AVATAR && !v->query[0]) x = Hint(x, G_SQUARE, tr("Download pack"));
2854 if (v->query[0]) x = Hint(x, G_SQUARE, tr("Clear search"));
2855 else if (folder.pages) x = Hint(x, G_CIRCLE, tr("Packs"));
2856 else if (Cats(cur) > 1 && !recent_mode) x = Hint(x, G_CIRCLE, tr("Categories"));
2857 else x = Hint(x, G_CIRCLE, tr("Back"));
2858 if (!v->results && !v->sort && count > 50) x = Hint2(x, G_L2, G_R2, tr("A-Z"));
2859 if (Base_List() && Base_List()->num_sorted) // what Select changes to
2860 x = Hint(x, G_SELECT, tr(v->sort == SORT_AZ ? "Newest first" : v->sort == SORT_NEWEST ? "Largest first" : "A-Z order"));
2861 if (cur == TYPE_THEME)
2862 font_draw_right(FONT_REGULAR, 16, SCREEN_W - LIST_X, LIST_Y + ROWS * ROW_H + 12,
2863 tr("Not every theme has a preview: some were archived without one."), C_DIM);
2864 if (!Draw_Strip() && count) {
2865 char pos[64], letter = '#';
2866 int at = View_Pos(v, v->sel);
2867 for (int i = 0; i < NUM_LETTERS; i++)
2868 if (l->letter_first[i] >= 0 && l->letter_first[i] <= at) letter = i ? 'A' + i - 1 : '#';
2869 snprintf(pos, sizeof(pos), "%c %d / %d", v->results || v->sort ? ' ' : letter, v->sel + 1, count);
2870 font_draw_right(FONT_MEDIUM, 19, SCREEN_W - LIST_X, SCREEN_H - 55, pos, C_DIM);
2871 }
2872}
2873
2874// a "key value" row of the detail screen (the value wraps); returns the y after it
2875static int Info_Row(int x, int y, int w, const char *key, const char *value, uint32_t c)
2876{
2877 char lines[3][256];
2878 font_draw(FONT_REGULAR, 19, x, y, key, C_DIM, 0);
2879 int n = Wrap(FONT_MEDIUM, 19, value, w - 124, lines, 3);
2880 for (int i = 0; i < n; i++)
2881 font_draw(FONT_MEDIUM, 19, x + 124, y + i * 26, lines[i], c, 0);
2882 return y + (n ? n : 1) * 26 + 10;
2883}
2884
2885/*
2886The detail screen, in two columns: on the left the preview (640x360) and under it
2887the download, how it went, or how to use it after; on the right the title, what
2888it is (author, size, where it saves...) and the description.
2889*/
2890#define DET_PX (LIST_X + 8)
2891#define DET_PY 84
2892#define DET_PW 640
2893#define DET_PH 360
2894
2895static void Draw_Detail(void)
2896{
2897 const cat_item_t *it = det;
2898 char line[400], size[128], dest[512];
2899 gfx_round_rect(LIST_X - 12, 60, SCREEN_W - 2 * LIST_X + 24, SCREEN_H - 150, 16, C_PANEL);
2900
2901 // the preview: a theme's or homebrew's filling the box, an avatar's square in the middle of it
2902 int sq = it->type == TYPE_AVATAR, pw = sq ? DET_PH : DET_PW, px = DET_PX + (DET_PW - pw) / 2, failed = 1;
2903 image_t *img = it->preview[0] ? Art_Get(it, 1, pw, DET_PH, &failed) : NULL;
2904 if (!img || sq) gfx_round_rect(DET_PX, DET_PY, DET_PW, DET_PH, 12, 0xff11151f);
2905 if (img)
2906 gfx_image(img, px, DET_PY, pw, DET_PH, sq ? 0 : 12, 255);
2907 else if (failed) {
2908 Icon_Draw(it, DET_PX + (DET_PW - ICON_BIG) / 2, DET_PY + (DET_PH - ICON_BIG) / 2 - (it->type == TYPE_THEME ? 20 : 0), ICON_BIG);
2909 if (it->type == TYPE_THEME)
2910 font_draw_center(FONT_REGULAR, 18, DET_PX + DET_PW / 2, DET_PY + DET_PH - 52,
2911 tr("This theme was archived without a preview."), C_DIM, DET_PW - 40);
2912 }
2913 else
2914 font_draw_center(FONT_REGULAR, 18, DET_PX + DET_PW / 2, DET_PY + DET_PH / 2 - 12, tr("Loading the preview..."), C_DIM, 0);
2915
2916 // what it is
2917 int x = DET_PX + DET_PW + 32, w = SCREEN_W - LIST_X - 8 - x, y = DET_PY;
2918 char lines[8][256];
2919 int n = Wrap(FONT_BOLD, 32, it->title, w, lines, 2);
2920 for (int i = 0; i < n; i++, y += 40)
2921 font_draw(FONT_BOLD, 32, x, y, lines[i], C_TEXT, 0);
2922 y += 14;
2923 if (it->author[0]) y = Info_Row(x, y, w, tr("Author"), it->author, C_TEXT);
2924 if (it->version[0]) y = Info_Row(x, y, w, tr("Version"), it->version, C_TEXT);
2925 Fmt_Size(it->size, size, sizeof(size));
2926 if (it->size >= 50 * 1024 * 1024) { // a big one: how long it takes at the speed downloads get here
2927 char when[64];
2928 Fmt_Time(it->size, when, sizeof(when));
2929 if (when[0]) snprintf(size + strlen(size), sizeof(size) - strlen(size), " (%s)", when);
2930 }
2931 y = Info_Row(x, y, w, tr("Size"), size, C_TEXT);
2932 if (it->group[0]) y = Info_Row(x, y, w, tr(it->type == TYPE_AVATAR ? "Pack" : "Category"), tr(it->group), C_TEXT);
2933 if (it->requires == REQ_CFW) y = Info_Row(x, y, w, tr("Works on"), tr("CFW only"), C_WARN); // (only when HEN won't do)
2934 int code = store_dest(it, dest, sizeof(dest));
2935 y = Info_Row(x, y, w, tr("Saves to"), code == DEST_OK ? dest : tr(store_dest_message(code)), code == DEST_OK ? C_TEXT : C_WARN);
2936 const char *have = store_downloaded(it);
2937 if (have) y = Info_Row(x, y, w, tr("Downloaded"), tr("Yes"), C_OK);
2938 const char *desc = Desc_Of(it); // (in the UI's language, when there's one)
2939 if (desc[0]) { // as much of it as fits above the panel's foot
2940 int max = (SCREEN_H - 112 - (y + 8)) / 28;
2941 n = max > 0 ? Wrap(FONT_REGULAR, 20, desc, w, lines, max > 8 ? 8 : max) : 0;
2942 for (int i = 0; i < n; i++)
2943 font_draw(FONT_REGULAR, 20, x, y + 8 + i * 28, lines[i], C_TEXT2, 0);
2944 }
2945
2946 // under the preview: the download, how it went, or how to use it after
2947 x = DET_PX;
2948 w = DET_PW;
2949 int by = DET_PY + DET_PH + 24, q = Queue_Find(it->id);
2950 if (q >= 0 && q == dl_q) {
2951 const char *what = tr(dl->state == DL_CHECKING ? "Checking what's there already..." :
2952 dl->state == DL_VERIFYING ? "Checking the sha256..." : "Downloading");
2953 float frac = dl->size ? (float)dl->got / (float)dl->size : 0;
2954 char got[32], tot[32], spd[32];
2955 Fmt_Size(dl->got, got, sizeof(got));
2956 Fmt_Size(dl->size, tot, sizeof(tot));
2957 Fmt_Size((long long)dl_speed, spd, sizeof(spd));
2958 gfx_round_rect(x, by, w, 14, 7, 0x40ffffff);
2959 gfx_round_rect(x, by, (int)(w * frac), 14, 7, C_ACCENT);
2960 char eta[64];
2961 Fmt_Eta(dl->size - dl->got, dl_speed, eta, sizeof(eta)); // (at the speed it's getting now)
2962 if (dl->state == DL_RUNNING && eta[0])
2963 snprintf(line, sizeof(line), "%s %d%% · %s of %s · %s/s · %s", what, (int)(frac * 100), got, tot, spd, eta);
2964 else if (dl->state == DL_RUNNING)
2965 snprintf(line, sizeof(line), "%s %d%% · %s of %s · %s/s", what, (int)(frac * 100), got, tot, spd);
2966 else
2967 snprintf(line, sizeof(line), "%s %d%% · %s of %s", what, (int)(frac * 100), got, tot);
2968 font_draw(FONT_MEDIUM, 20, x, by + 26, line, C_TEXT, w);
2969 if (dl->resumed_from) {
2970 Fmt_Size(dl->resumed_from, got, sizeof(got));
2971 snprintf(line, sizeof(line), tr("Resumed from %s"), got);
2972 font_draw(FONT_REGULAR, 17, x, by + 56, line, C_DIM, w);
2973 }
2974 } else if (Queue_Active(q)) {
2975 int ahead = Queue_Ahead(q);
2976 snprintf(line, sizeof(line), tr(ahead == 1 ? "Queued: %d download ahead of it. You can go back and keep browsing." :
2977 "Queued: %d downloads ahead of it. You can go back and keep browsing."), ahead);
2978 n = Wrap(FONT_MEDIUM, 20, line, w, lines, 3);
2979 for (int i = 0; i < n; i++)
2980 font_draw(FONT_MEDIUM, 20, x, by + i * 28, lines[i], C_TEXT, 0);
2981 } else if (result_msg[0] || q >= 0) {
2982 const char *msg = result_msg[0] ? result_msg : queue[q].msg;
2983 int ok = result_msg[0] ? result_ok : queue[q].ok;
2984 n = Wrap(FONT_MEDIUM, 20, msg, w, lines, 4);
2985 for (int i = 0; i < n; i++)
2986 font_draw(FONT_MEDIUM, 20, x, by + i * 28, lines[i], ok == 1 ? C_OK : ok == 2 ? C_WARN : C_ERR, 0);
2987 } else {
2988 snprintf(line, sizeof(line), tr("After it downloads: %s"), Install_Hint(it->type));
2989 n = Wrap(FONT_REGULAR, 19, line, w, lines, 3);
2990 for (int i = 0; i < n; i++)
2991 font_draw(FONT_REGULAR, 19, x, by + i * 27, lines[i], C_DIM, 0);
2992 }
2993
2994 // the download first, then the rest
2995 int hx = LIST_X;
2996 if (Queue_Active(q)) hx = Hint(hx, G_SQUARE, tr("Cancel download"));
2997 else {
2998 int failed = result_msg[0] ? !result_ok : q >= 0 && !queue[q].ok; // (result_ok 2: a question, not a failure)
2999 hx = Hint(hx, G_CROSS, tr(failed ? "Try again" : have ? "Download again" : "Download"));
3000 }
3001 if (it->preview[0]) hx = Hint(hx, G_TRIANGLE, tr("Full screen"));
3002 if (strcmp(it->id, "ps3world-app")) hx = Hint(hx, G_SELECT, tr(Fav_Find(it->id) >= 0 ? "Unfavourite" : "Favourite"));
3003 Hint(hx, G_CIRCLE, tr("Back"));
3004 if (q != dl_q) Draw_Strip(); // another one downloading
3005}
3006
3007// the preview as big as the screen takes it (an avatar's square, in the middle)
3008static void Draw_Preview_Full(void)
3009{
3010 const cat_item_t *it = det;
3011 int sq = it->type == TYPE_AVATAR, w = sq ? 600 : SCREEN_W, h = sq ? 600 : SCREEN_H, failed = 1;
3012 gfx_rect(0, 0, SCREEN_W, SCREEN_H, 0xff000000);
3013 image_t *img = Art_Get(it, 1, w, h, &failed);
3014 if (img) gfx_image(img, (SCREEN_W - w) / 2, (SCREEN_H - h) / 2, w, h, 0, 255);
3015 else font_draw_center(FONT_REGULAR, 22, SCREEN_W / 2, SCREEN_H / 2 - 12,
3016 failed ? tr("The preview couldn't be loaded.") : tr("Loading the preview..."), C_DIM, 0);
3017 Hint(LIST_X, G_CIRCLE, tr("Back"));
3018}
3019
3020static void Draw_Profile(void)
3021{
3022 font_draw_center(FONT_BOLD, 30, SCREEN_W / 2, 140, tr("Which user is the avatar for?"), C_TEXT, 0);
3023 font_draw_center(FONT_REGULAR, 19, SCREEN_W / 2, 186, tr("It's saved in that user's psn_avatar folder."), C_DIM, 0);
3024 // 7 rows fit above the hints: the list scrolls to keep the chosen one on screen
3025 int top = profile_sel > 6 ? profile_sel - 6 : 0;
3026 for (int i = top; i < num_profiles && i < top + 7; i++) {
3027 int y = 250 + (i - top) * 56;
3028 if (i == profile_sel) gfx_round_rect(SCREEN_W / 2 - 300, y - 8, 600, 48, 12, C_ROW_ON);
3029 char line[160];
3030 snprintf(line, sizeof(line), "%s (%s)", profile_names[i], profiles[i]);
3031 font_draw_center(FONT_MEDIUM, 22, SCREEN_W / 2, y, line, C_TEXT, 580);
3032 }
3033 int x = Hint(LIST_X, G_CROSS, tr("Choose"));
3034 Hint(x, G_CIRCLE, tr("Back"));
3035}
3036
3037static void Draw_News(void)
3038{
3039 char lines[2][256];
3040 Draw_Top(tr("What's new"));
3041 gfx_round_rect(LIST_X - 12, 96, LIST_W + 24, 446, 16, C_PANEL);
3042 font_draw_center(FONT_BOLD, 38, SCREEN_W / 2, 128, APP_NAME " " APP_VERSION, C_TEXT, LIST_W - 40);
3043 int y = 206;
3044 for (int i = 0; i < NUM_NEWS; i++) {
3045 int n = Wrap(FONT_MEDIUM, 22, tr(news_lines[i]), LIST_W - 180, lines, 2); // (a long translation on two lines)
3046 gfx_circle(LIST_X + 70, y + 12, 6, C_CYAN);
3047 for (int k = 0; k < n; k++)
3048 font_draw(FONT_MEDIUM, 22, LIST_X + 96, y + k * 30, lines[k], C_TEXT, 0);
3049 y += n * 30 + 24;
3050 }
3051 Hint(LIST_X, G_CROSS, tr("Continue"));
3052}
3053
3054static void Draw_Credits(void)
3055{
3056 char lines[4][256];
3057 int cx = SCREEN_W / 2;
3058 Draw_Top(tr("Credits & contact"));
3059 gfx_round_rect(LIST_X - 12, 96, LIST_W + 24, 446, 16, C_PANEL);
3060 font_draw_center(FONT_MEDIUM, 20, cx, 136, tr("Made by"), C_CYAN, 0);
3061 font_draw_center(FONT_BOLD, 32, cx, 166, "daedra000 \xc2\xb7 Claude (Anthropic)", C_TEXT, LIST_W - 40);
3062
3063 font_draw_center(FONT_MEDIUM, 20, cx, 246, tr("Thanks to"), C_CYAN, 0);
3064 int n = Wrap(FONT_REGULAR, 19, "Poppins & DejaVu fonts \xc2\xb7 Kenney's input prompts \xc2\xb7 "
3065 "Mbed TLS \xc2\xb7 cJSON \xc2\xb7 stb \xc2\xb7 PSL1GHT", LIST_W - 120, lines, 2);
3066 for (int i = 0; i < n; i++)
3067 font_draw_center(FONT_REGULAR, 19, cx, 276 + i * 28, lines[i], C_TEXT2, 0);
3068
3069 gfx_rect(cx - 300, 366, 600, 1, C_BORDER);
3070 int tx = cx - (qr_contact ? QR_PX / 2 + 20 : 0), tw = qr_contact ? 700 : LIST_W - 160;
3071 n = Wrap(FONT_MEDIUM, 22, tr("Want your homebrew, theme or avatar on PS3 World? Send it in with the contact form:"), tw, lines, 3);
3072 for (int i = 0; i < n; i++)
3073 font_draw_center(FONT_MEDIUM, 22, tx, 392 + i * 32, lines[i], C_TEXT, 0);
3074 font_draw_center(FONT_BOLD, 34, tx, 404 + n * 32, "ps3.world/contact", C_CYAN, 0);
3075 if (qr_contact) {
3076 int qx = tx + tw / 2 + 30, qy = 376;
3077 gfx_image(qr_contact, qx, qy, QR_PX, QR_PX, 8, 255);
3078 font_draw_center(FONT_REGULAR, 16, qx + QR_PX / 2, qy + QR_PX + 6, tr("Scan with your phone"), C_DIM, QR_PX + 60);
3079 }
3080
3081 Hint(LIST_X, G_CIRCLE, tr("Back"));
3082}
3083
3084// a flag at x, y (56x42, rounded), or a grey box when it's missing
3085static void Flag(int lang, int x, int y)
3086{
3087 if (flag_img[lang]) gfx_image(flag_img[lang], x, y, 56, 42, 6, 255);
3088 else gfx_round_rect(x, y, 56, 42, 6, 0xff2a3040);
3089}
3090
3091// the one in use: a cyan dot at the card's far end
3092static void In_Use(int x, int y, int w, int h)
3093{
3094 gfx_circle(x + w - 22, y + h / 2, 6, C_CYAN);
3095}
3096
3097static void Draw_Lang(void)
3098{
3099 char line[160];
3100 int sys = i18n_system(), choice = i18n_choice();
3101 Draw_Top(tr("Language"));
3102
3103 // the PS3's language
3104 Card_Frame(LIST_X, LG_TOP, LIST_W, 60, lang_sel == 0);
3105 Flag(sys, LIST_X + 16, LG_TOP + 9);
3106 snprintf(line, sizeof(line), tr("The PS3's language (%s)"), lang_name(sys));
3107 font_draw(FONT_BOLD, 22, LIST_X + 88, LG_TOP + 19, line, choice < 0 ? C_CYAN : C_TEXT, LIST_W - 140);
3108 if (choice < 0) In_Use(LIST_X, LG_TOP, LIST_W, 60);
3109
3110 // the languages
3111 for (int i = 0; i < LANG_COUNT; i++) {
3112 int x = LIST_X + (i % LG_COLS) * (LG_W + CARD_GAP), y = LG_ROW(i / LG_COLS);
3113 Card_Frame(x, y, LG_W, LG_H, lang_sel == i + 1);
3114 Flag(i, x + 14, y + (LG_H - 42) / 2);
3115 int room = LG_W - 84 - (i == choice ? 34 : 14);
3116 image_t *ni = name_img[i][i == choice && name_img[i][1]];
3117 if (ni) // (a picture: drawing the name would load the PS3's CJK fonts, a second or two; its baseline 23 down, the text's 17)
3118 gfx_image(ni, x + 84, y + 13 + 17 - 23, name_w[i] < room ? name_w[i] : room, 32, 0, 255);
3119 else {
3120 int npx = font_width(FONT_BOLD, 22, lang_name(i)) > room ? 19 : 22; // (a long name a size smaller)
3121 font_draw(FONT_BOLD, npx, x + 84, y + 13 + (22 - npx) / 2, lang_name(i), i == choice ? C_CYAN : C_TEXT, room);
3122 }
3123 font_draw(FONT_REGULAR, 16, x + 84, y + 43, lang_english(i), C_DIM, LG_W - 84 - 34);
3124 if (i == choice) In_Use(x, y, LG_W, LG_H);
3125 }
3126
3127 // the sounds
3128 Card_Frame(LIST_X, LG_SOUND, LIST_W, 52, lang_sel == LANG_COUNT + 1);
3129 font_draw(FONT_MEDIUM, 21, LIST_X + 24, LG_SOUND + 15, tr(sound_on ? "Sound effects: on" : "Sound effects: off"), C_TEXT, LIST_W - 80);
3130 gfx_round_rect(LIST_X + LIST_W - 70, LG_SOUND + 15, 44, 22, 11, sound_on ? C_ACCENT : 0xff3a4152); // a switch
3131 gfx_circle(LIST_X + LIST_W - (sound_on ? 37 : 59), LG_SOUND + 26, 8, C_TEXT);
3132
3133 int x = Hint(LIST_X, G_CROSS, tr("Choose"));
3134 Hint(x, G_CIRCLE, tr("Back"));
3135}
3136
3137static void Draw_Favourites(void)
3138{
3139 const char *tabs[2] = {tr("My downloads"), tr("Favourites")};
3140 Draw_Top(NULL);
3141 Tabs(LIST_X + 236, 38, TOP_TABS_MAX, tabs, 2, 1);
3142 int n = favs ? favs->count : 0;
3143 char line[300], size[32];
3144 snprintf(line, sizeof(line), tr(n == 1 ? "%d favourite" : "%d favourites"), n);
3145 font_draw(FONT_MEDIUM, 20, LIST_X, 104, line, C_DIM, 0);
3146 gfx_round_rect(LIST_X - 12, LIST_Y - 8, LIST_W + 24, ROWS * ROW_H + 16, 14, C_PANEL);
3147 if (!n)
3148 font_draw_center(FONT_MEDIUM, 22, SCREEN_W / 2, LIST_Y + 180, tr("No favourites yet. Press Select on an item to add it."), C_DIM, SCREEN_W - 200);
3149 for (int row = dls_top; row < n && row < dls_top + ROWS; row++) {
3150 const cat_item_t *it = &favs->items[row];
3151 int y = LIST_Y + (row - dls_top) * ROW_H, rx = LIST_X + LIST_W - 12;
3152 int thumbs = it->type == TYPE_THEME, pw = thumbs ? THUMB_W : ICON_LIST, ph = thumbs ? THUMB_H : ICON_LIST;
3153 int tx = LIST_X + 6 + pw + 16, iy = y + (ROW_H - 4 - ph) / 2;
3154 if (row == dls_sel) gfx_round_rect(LIST_X - 4, y, LIST_W + 8, ROW_H - 4, 10, C_ROW_ON);
3155 if (thumbs) Thumb_Draw(it, it->title, LIST_X + 6, iy);
3156 else Icon_Draw(it, LIST_X + 6, iy, ICON_LIST);
3157 Fmt_Size(it->size, size, sizeof(size));
3158 rx -= font_draw_right(FONT_MEDIUM, 19, rx, y + 22, size, C_DIM) + 14;
3159 if (store_downloaded(it)) Badge(&rx, y + 20, tr("Downloaded"), C_OK);
3160 snprintf(line, sizeof(line), "%s · %s", tr(section_names[it->type]), it->type == TYPE_HOMEBREW ? (it->author[0] ? it->author : tr("Unknown author")) : tr(it->group));
3161 font_draw(FONT_MEDIUM, 22, tx, y + 8, it->title, C_TEXT, rx - tx - 10);
3162 font_draw(FONT_REGULAR, 17, tx, y + 37, line, C_DIM, rx - tx - 10);
3163 }
3164 int x = LIST_X;
3165 if (n) x = Hint(x, G_CROSS, tr("Open"));
3166 if (n) x = Hint(x, G_SQUARE, tr("Unfavourite"));
3167 Hint(x, G_CIRCLE, tr("Back"));
3168}
3169
3170static void Draw_Downloads(void)
3171{
3172 if (dls_tab == 1) { Draw_Favourites(); return; }
3173 const char *tabs[2] = {tr("My downloads"), tr("Favourites")};
3174 Draw_Top(NULL);
3175 Tabs(LIST_X + 236, 38, TOP_TABS_MAX, tabs, 2, 0);
3176 int n = Dls_Count(), qrows = Dls_Queue_Rows();
3177 long long total = 0;
3178 store_rec_t r;
3179 for (int i = 0; store_download_at(i, &r); i++) total += r.size;
3180 char line[400], size[32], when[32], fr[32];
3181 Fmt_Size(total, size, sizeof(size));
3182 Fmt_Size(free_bytes, fr, sizeof(fr));
3183 if (free_bytes >= 0) snprintf(line, sizeof(line), tr("%d on the HDD (%s) · %s free"), store_num_downloads(), size, fr);
3184 else snprintf(line, sizeof(line), tr("%d on the HDD (%s)"), store_num_downloads(), size);
3185 font_draw(FONT_MEDIUM, 20, LIST_X, 104, line, C_DIM, 0);
3186 gfx_round_rect(LIST_X - 12, LIST_Y - 8, LIST_W + 24, ROWS * ROW_H + 16, 14, C_PANEL);
3187 if (!n)
3188 font_draw_center(FONT_MEDIUM, 22, SCREEN_W / 2, LIST_Y + 180, tr("Nothing downloaded yet."), C_DIM, SCREEN_W - 200);
3189 int can_open = 0, is_queue = dls_sel < qrows, q_active = 0;
3190 for (int row = dls_top; row < n && row < dls_top + ROWS; row++) {
3191 int y = LIST_Y + (row - dls_top) * ROW_H, tx = LIST_X + 6 + ICON_LIST + 16, rx = LIST_X + LIST_W - 12;
3192 const char *title, *sub;
3193 char subbuf[400];
3194 if (row == dls_sel) gfx_round_rect(LIST_X - 4, y, LIST_W + 8, ROW_H - 4, 10, C_ROW_ON);
3195 if (row < qrows) { // in the queue: waiting, downloading, or failed
3196 const queued_t *q = &queue[Dls_Queue_At(row)];
3197 title = q->it->title;
3198 Fmt_Size(q->it->size, size, sizeof(size));
3199 rx -= font_draw_right(FONT_MEDIUM, 19, rx, y + 22, size, C_DIM) + 14;
3200 if (q->state == Q_RUNNING && dl) {
3201 snprintf(subbuf, sizeof(subbuf), tr("Downloading %d%%"), dl->size ? (int)(dl->got * 100 / dl->size) : 0);
3202 Badge(&rx, y + 20, subbuf, C_ACCENT);
3203 } else if (q->state == Q_WAITING) Badge(&rx, y + 20, tr("Queued"), C_DIM);
3204 else Badge(&rx, y + 20, tr("Failed"), C_ERR);
3205 sub = q->state == Q_FAILED ? q->msg : q->dest;
3206 if (row == dls_sel) { can_open = 1; q_active = q->state <= Q_RUNNING; }
3207 } else {
3208 store_download_at(row - qrows, &r);
3209 const char *file = strrchr(r.path, '/');
3210 title = r.title[0] ? r.title : file ? file + 1 : r.path;
3211 Fmt_Size(r.size, size, sizeof(size));
3212 rx -= font_draw_right(FONT_MEDIUM, 19, rx, y + 22, size, C_DIM) + 14;
3213 if (Dls_Newer(&r)) Badge(&rx, y + 20, tr("New version"), C_WARN);
3214 Fmt_Date(r.when, when, sizeof(when));
3215 snprintf(subbuf, sizeof(subbuf), "%s%s%s", r.path, when[0] ? " · " : "", when);
3216 sub = subbuf;
3217 if (row == dls_sel) can_open = Homebrew_Find(r.id) != NULL;
3218 }
3219 Placeholder(title, LIST_X + 6, y + (ROW_H - 4 - ICON_LIST) / 2, ICON_LIST);
3220 font_draw(FONT_MEDIUM, 22, tx, y + 8, title, C_TEXT, rx - tx - 10);
3221 if (row == dls_confirm)
3222 font_draw(FONT_MEDIUM, 17, tx, y + 37, tr("Press Square again to delete this file from the HDD."), C_WARN, rx - tx - 10);
3223 else
3224 font_draw(FONT_REGULAR, 17, tx, y + 37, sub, C_DIM, rx - tx - 10);
3225 }
3226 if (n > ROWS) {
3227 int h = ROWS * ROW_H, bh = h * ROWS / n;
3228 if (bh < 24) bh = 24;
3229 int by = LIST_Y + (int)((long long)(h - bh) * dls_sel / (n - 1));
3230 gfx_round_rect(LIST_X + LIST_W + 2, by, 4, bh, 2, 0x99ffffff);
3231 }
3232 int x = LIST_X;
3233 if (can_open) x = Hint(x, G_CROSS, tr("Open"));
3234 if (n) x = Hint(x, G_SQUARE, tr(!is_queue ? "Delete" : q_active ? "Cancel download" : "Clear"));
3235 Hint(x, G_CIRCLE, tr("Back"));
3236}
3237
3238void ui_render(void)
3239{
3240 gfx_mirror(lang_current() == LANG_AR); // Arabic: the whole screen right to left
3241 if (backdrop) gfx_image(backdrop, 0, 0, SCREEN_W, SCREEN_H, 0, 255);
3242 else gfx_clear(C_BG);
3243 switch (screen) {
3244 case SCR_START:
3245 if (logo) gfx_image(logo, (SCREEN_W - 360) / 2, 230, 360, 51, 0, 255);
3246 {
3247 char lines[4][256];
3248 int n = Wrap(FONT_MEDIUM, 24, start_msg, SCREEN_W - 300, lines, 4);
3249 for (int i = 0; i < n; i++)
3250 font_draw_center(FONT_MEDIUM, 24, SCREEN_W / 2, 330 + i * 34, lines[i], start_err ? C_ERR : C_TEXT, 0);
3251 }
3252 if (start_err) Hint(SCREEN_W / 2 - 90, G_CROSS, tr("Try again"));
3253 break;
3254 case SCR_HOME: Draw_Home(); break;
3255 case SCR_LIST:
3256 if (Picking()) Draw_Picker();
3257 else Draw_List();
3258 break;
3259 case SCR_DETAIL: if (det) { if (preview_full) Draw_Preview_Full(); else Draw_Detail(); } break;
3260 case SCR_PROFILE: Draw_Profile(); break;
3261 case SCR_DOWNLOADS: Draw_Downloads(); break;
3262 case SCR_LANG: Draw_Lang(); break;
3263 case SCR_CREDITS: Draw_Credits(); break;
3264 case SCR_NEWS: Draw_News(); break;
3265 }
3266 font_draw_right(FONT_REGULAR, 16, SCREEN_W - LIST_X, 40, APP_NAME " " APP_VERSION, C_DIM);
3267 if (!intro_done) Draw_Intro();
3268}
3269
3270void ui_shutdown(void)
3271{
3272 // a download in progress keeps what's arrived: it resumes next time (the queue is forgotten)
3273 if (dl) dl->pause = 1;
3274 jobs_wait_lane(LANE_DL, 3000000);
3275}