| 1 | /* |
| 2 | ui.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 |
|
| 15 | Lists are never loaded whole: the index says how many items a list has and |
| 16 | which page file holds each 100, and a page is fetched (or read from the HDD |
| 17 | cache) when the list reaches it, a few kept in memory. Search reads the list's |
| 18 | search.txt (one line an item) instead of every page. |
| 19 |
|
| 20 | The index on the HDD opens at once; the server's is fetched in the background |
| 21 | and replaces it when it's newer. Cross confirms and Circle goes back, as in |
| 22 | Resonant (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 |
|
| 84 | enum { SCR_START, SCR_HOME, SCR_LIST, SCR_DETAIL, SCR_PROFILE, SCR_DOWNLOADS, SCR_LANG, SCR_CREDITS, SCR_NEWS }; |
| 85 | static int news_due; // this version's What's new is shown when the catalogue's in (once: USRDIR/seen.txt) |
| 86 |
|
| 87 | static int screen = SCR_START; |
| 88 | static int dirty = 1; |
| 89 | static uint64_t t_now; |
| 90 | static image_t *backdrop, *logo, *qr_contact, *flag_img[LANG_COUNT]; |
| 91 | static 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 |
|
| 98 | static 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) |
| 99 | static int name_w[LANG_COUNT]; // (qr: ps3.world/contact, made by tools/qr.mjs) |
| 100 |
|
| 101 | static const char *section_names[NUM_TYPES] = {"Homebrew", "Themes", "Avatars"}; |
| 102 |
|
| 103 | /* |
| 104 | ================== |
| 105 | Helpers |
| 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) |
| 110 | static 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 |
|
| 130 | static 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 |
|
| 142 | static 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 |
|
| 150 | static 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 |
|
| 156 | static char Lower(char c) { return c >= 'A' && c <= 'Z' ? c + 32 : c; } |
| 157 |
|
| 158 | // needle in the first len bytes of hay (any case) |
| 159 | static 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) |
| 171 | static 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 |
| 183 | static 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) |
| 218 | enum { 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 }; |
| 219 | static const char *glyph_files[NUM_GLYPHS] = {"cross", "circle", "square", "triangle", "l1", "r1", "l2", "r2", "dpad_ud", "select", "start"}; |
| 220 | static image_t *glyphs[NUM_GLYPHS]; |
| 221 |
|
| 222 | static 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 |
| 231 | static 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 |
|
| 238 | static 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 |
| 247 | static 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 | ================== |
| 275 | Cards, as ps3.world's .explore-card: the art faded in from the right, a cyan |
| 276 | kicker, the title, a line or two, and chips (data/cards: tools/make-art.mjs) |
| 277 | ================== |
| 278 | */ |
| 279 | typedef struct { |
| 280 | const char *list, *art, *kicker, *title, *blurb; |
| 281 | } card_text_t; |
| 282 |
|
| 283 | static 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 | }; |
| 288 | static 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 |
|
| 298 | static image_t *card_bg, *home_art[NUM_TYPES], *cat_art[NUM_CAT_CARDS]; |
| 299 |
|
| 300 | static 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 |
| 309 | static 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 |
|
| 319 | static 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 |
| 330 | static int Cap_Top(int px) { return px * 22 / 100; } |
| 331 | static 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) |
| 334 | static 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 |
| 363 | static 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 |
|
| 370 | static 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 | ================== |
| 390 | Pages: 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 |
|
| 396 | typedef 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 |
|
| 407 | typedef 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 |
|
| 414 | static file_slot_t files[PAGE_SLOTS]; |
| 415 |
|
| 416 | typedef struct { |
| 417 | cat_file_t f; |
| 418 | int search; |
| 419 | } file_req_t; |
| 420 |
|
| 421 | static 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 |
|
| 430 | static 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 |
|
| 461 | static 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 |
|
| 469 | static 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 |
| 491 | static 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 | ================== |
| 525 | The index, sections and their lists |
| 526 | ================== |
| 527 | */ |
| 528 | static cat_index_t *ix; |
| 529 | static cat_index_t *pending; // a newer one, used when the detail screen isn't open |
| 530 | static job_t *fetch_job; |
| 531 | static char banner[200]; // "Offline: ..." over the list |
| 532 | static int search_banner; // (it's "Search isn't available") |
| 533 | static char start_msg[300]; // the start screen's line |
| 534 | static int start_err; |
| 535 |
|
| 536 | typedef 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 |
|
| 545 | static view_t *views[NUM_TYPES]; // one per list |
| 546 | static int cur; // the section shown |
| 547 | static int cur_list[NUM_TYPES]; // its list, or -1 for its category picker |
| 548 | static int picker_sel, picker_top; // (picker_top: the first row of tiles shown) |
| 549 | static int home_sel; |
| 550 | static int recent_mode; // the list is a Recently added, from the home screen (L1/R1: the sections') |
| 551 | static cat_list_t folder; // an avatar pack opened from a list, shown as a list (pages NULL: none) |
| 552 | static view_t folder_view; |
| 553 |
|
| 554 | static 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 |
| 556 | static 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 |
| 563 | static int Cats(int t) { return ix ? ix->num_lists[t] - (Recent_Of(t) >= 0) : 0; } |
| 564 | static int Cat_List(int t, int i) { int r = Recent_Of(t); return r >= 0 && i >= r ? i + 1 : i; } |
| 565 | static int List_Cat(int t, int l) { int r = Recent_Of(t); return r >= 0 && l > r ? l - 1 : l; } |
| 566 | static int Picking(void) { return ix && Cats(cur) > 1 && cur_list[cur] < 0; } |
| 567 | // the list picked (in A-Z order) |
| 568 | static 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 | } |
| 573 | static 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 |
| 579 | static 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 |
|
| 587 | static void Folder_Close(void) |
| 588 | { |
| 589 | free(folder_view.results); |
| 590 | memset(&folder_view, 0, sizeof(folder_view)); |
| 591 | catalog_free_list(&folder); |
| 592 | } |
| 593 | static int View_Count(const view_t *v, const cat_list_t *l) { return v->results ? v->num_results : l->count; } |
| 594 | static 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 |
| 599 | static int Gridded(int t) { return t == TYPE_THEME || t == TYPE_AVATAR; } |
| 600 | static int Grid_Cols(int t) { return t == TYPE_THEME ? 5 : 8; } |
| 601 | static int Grid_W(int t) { return (LIST_W - CARD_GAP * (Grid_Cols(t) - 1)) / Grid_Cols(t); } |
| 602 | static 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) |
| 605 | static 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 | } |
| 622 | static 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) |
| 625 | static 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 |
| 638 | static 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 |
|
| 648 | static 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 |
|
| 658 | static 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 |
|
| 693 | static 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 |
|
| 712 | static void Fetch_Cleanup(job_t *j) |
| 713 | { |
| 714 | catalog_free_index(j->result); |
| 715 | } |
| 716 |
|
| 717 | static 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 |
|
| 729 | static 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) |
| 762 | static 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 |
| 768 | static 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 | ================== |
| 797 | Icons and previews: from the HDD cache, or fetched once and cached |
| 798 | ================== |
| 799 | */ |
| 800 | #define ICON_SLOTS 96 |
| 801 | #define ICON_INFLIGHT 12 |
| 802 |
|
| 803 | typedef 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 |
| 812 | static icon_slot_t icons[ICON_SLOTS]; |
| 813 | static int icons_inflight; |
| 814 |
|
| 815 | typedef struct { |
| 816 | char url[1100]; |
| 817 | char path[600]; |
| 818 | int w, h; |
| 819 | } icon_req_t; |
| 820 |
|
| 821 | static 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 |
|
| 841 | static void Icon_Cleanup(job_t *j) |
| 842 | { |
| 843 | free(j->arg); |
| 844 | image_free(j->result); |
| 845 | } |
| 846 |
|
| 847 | static 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) |
| 864 | static 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 |
|
| 907 | static image_t *Icon_Get(const cat_item_t *it, int size) { return Art_Get(it, 0, size, size, NULL); } |
| 908 |
|
| 909 | static 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 |
|
| 916 | static 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 |
|
| 927 | static 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 | ================== |
| 941 | Downloads: a queue, one at a time. Each entry keeps how it went (a finished |
| 942 | one is dropped when the queue needs its place) for its detail screen. |
| 943 | ================== |
| 944 | */ |
| 945 | #define QUEUE_MAX 128 // (a big avatar pack fits) |
| 946 | enum { Q_WAITING, Q_RUNNING, Q_DONE, Q_FAILED }; |
| 947 |
|
| 948 | typedef 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 |
|
| 955 | static queued_t queue[QUEUE_MAX]; |
| 956 | static int queue_n; |
| 957 | static cat_item_t *det; // the detail screen's item (a copy: its page may go) |
| 958 | static download_t *dl; // the running one, queue[dl_q] |
| 959 | static job_t *dl_job; |
| 960 | static int dl_q = -1; |
| 961 | static uint64_t dl_t_last; |
| 962 | static long long dl_last_got; |
| 963 | static double dl_speed; // bytes a second, smoothed |
| 964 | static char result_msg[400]; // why the detail screen's item can't be queued (no profile...) |
| 965 | static int result_ok; |
| 966 | static int det_back = SCR_LIST; // where Circle goes from the detail screen |
| 967 | static int preview_full; // the detail screen's preview, filling the screen (Triangle) |
| 968 | static int big_asked; // Cross pressed once on a big download: the second press starts it |
| 969 | static uint64_t dl_started; // when the running download started (for the speed it got) |
| 970 | static 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 |
| 974 | static 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) |
| 985 | static void Fmt_Time(long long bytes, char *out, int size) { Fmt_Eta(bytes, speed_seen, out, size); } |
| 986 |
|
| 987 | static 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 |
|
| 996 | static 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 | } |
| 1003 | static long long free_bytes = -1; // on /dev_hdd0, read every few seconds (home and My downloads) |
| 1004 | static uint64_t free_checked; |
| 1005 |
|
| 1006 | static void Dl_Run(job_t *j) { download_run(j->arg); } |
| 1007 | static void Profiles_Load(void); |
| 1008 |
|
| 1009 | // the newest entry for an item, or -1 |
| 1010 | static 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 |
| 1018 | static int Queue_Active(int i) { return i >= 0 && i < queue_n && queue[i].state <= Q_RUNNING; } |
| 1019 | static 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 |
|
| 1026 | static 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 |
| 1035 | static 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) |
| 1079 | static 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) |
| 1101 | static 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 |
| 1110 | static 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 |
| 1118 | static 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 |
|
| 1168 | static 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) |
| 1184 | static void Note(const char *msg) |
| 1185 | { |
| 1186 | snprintf(banner, sizeof(banner), "%s", msg); |
| 1187 | search_banner = 1; |
| 1188 | dirty = 1; |
| 1189 | } |
| 1190 |
|
| 1191 | /* |
| 1192 | ================== |
| 1193 | Homebrew's descriptions in the UI's language (the catalogue's lang/<code>.json: id to text), |
| 1194 | fetched and checked like a page; an item without one shows the English |
| 1195 | ================== |
| 1196 | */ |
| 1197 | static cJSON *desc_root; // the language in use's, or NULL |
| 1198 | static unsigned char desc_sha[32], desc_want[32]; |
| 1199 | static job_t *desc_job; |
| 1200 | static uint64_t desc_failed_at; |
| 1201 |
|
| 1202 | static 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 |
|
| 1211 | static void Desc_Cleanup(job_t *j) |
| 1212 | { |
| 1213 | free(j->arg); |
| 1214 | cJSON_Delete(j->result); |
| 1215 | } |
| 1216 |
|
| 1217 | static 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 |
|
| 1256 | static 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 | ================== |
| 1264 | Favourites: items starred on the detail screen (Select), kept in USRDIR/favourites.json |
| 1265 | as a catalogue page, so they open and download without their own pages |
| 1266 | ================== |
| 1267 | */ |
| 1268 | static catalog_t *favs; |
| 1269 |
|
| 1270 | static 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 |
|
| 1283 | static 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 |
|
| 1290 | static 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 |
| 1314 | static 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 | ================== |
| 1339 | A whole avatar pack: its pages fetched, then each avatar not already here queued |
| 1340 | ================== |
| 1341 | */ |
| 1342 | static cat_list_t pack; // the pack being queued (pages NULL: none) |
| 1343 | static int pack_profile, pack_back; // the profile screen is up for it; the screen it came from |
| 1344 |
|
| 1345 | static void Pack_End(void) |
| 1346 | { |
| 1347 | catalog_free_list(&pack); |
| 1348 | pack_profile = 0; |
| 1349 | } |
| 1350 |
|
| 1351 | static 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 |
|
| 1364 | static 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 |
|
| 1406 | static 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 |
|
| 1416 | static 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 | ================== |
| 1475 | Profiles (avatars) |
| 1476 | ================== |
| 1477 | */ |
| 1478 | static char profiles[16][64], profile_names[16][64]; |
| 1479 | static int num_profiles, profile_sel; |
| 1480 |
|
| 1481 | static 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 | ================== |
| 1496 | My downloads: the queue (what hasn't finished), then what's on the HDD, newest first |
| 1497 | ================== |
| 1498 | */ |
| 1499 | static int dls_sel, dls_top, dls_confirm = -1; // dls_confirm: the row Square was pressed on once |
| 1500 | static int dls_tab; // 0 the downloads, 1 the favourites (L1/R1) |
| 1501 |
|
| 1502 | static 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 |
| 1510 | static 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 |
|
| 1517 | static 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) |
| 1520 | static 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) |
| 1534 | static 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 |
|
| 1543 | static 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 | ================== |
| 1616 | The app's own update, from the catalogue |
| 1617 | ================== |
| 1618 | */ |
| 1619 | static 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 |
|
| 1629 | static 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 |
| 1632 | static 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 | ================== |
| 1655 | Input |
| 1656 | ================== |
| 1657 | */ |
| 1658 | static int searching; |
| 1659 | static int sound_on = 1; // (USRDIR/sound.txt: "off" turns them off) |
| 1660 |
|
| 1661 | static 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 |
|
| 1670 | static 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 | } |
| 1678 | static int lang_sel; // the language screen's row: 0 the PS3's, then LANG_* + 1 |
| 1679 |
|
| 1680 | static 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 |
| 1698 | static int Picker_Cols(void) { return cur == TYPE_HOMEBREW ? 3 : TILE_COLS; } |
| 1699 | static 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) |
| 1702 | static 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 |
|
| 1709 | static 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 |
|
| 1724 | static 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 |
| 1730 | static 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 |
|
| 1739 | static 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 |
|
| 1751 | static 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) |
| 1760 | static 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 |
|
| 1769 | static uint64_t quit_asked; // when Circle was pressed on the home screen (again soon after: quit) |
| 1770 |
|
| 1771 | static 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 | ================== |
| 1817 | Language: the PS3's (the first row), or one of ours |
| 1818 | ================== |
| 1819 | */ |
| 1820 | /* |
| 1821 | ================== |
| 1822 | What's new: this version's changes, once, the first time it's opened after an update |
| 1823 | ================== |
| 1824 | */ |
| 1825 | static 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 |
|
| 1830 | static 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 |
| 1839 | static 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 |
|
| 1851 | static 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 |
|
| 1860 | static 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 |
| 1867 | static 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 | ================== |
| 1894 | Surprise me: something at random from the section (its whole list, or for homebrew any of its |
| 1895 | categories by their size); opened when its page is here |
| 1896 | ================== |
| 1897 | */ |
| 1898 | static int surprise_list = -1, surprise_pos; |
| 1899 |
|
| 1900 | static 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 |
|
| 1921 | static 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 |
|
| 1943 | static 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 |
|
| 1969 | static 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 |
|
| 2090 | static 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 |
|
| 2102 | static 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 |
|
| 2118 | static 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 | ================== |
| 2136 | Frame |
| 2137 | ================== |
| 2138 | */ |
| 2139 | void 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) |
| 2185 | static void Ticker_Build(void); |
| 2186 | static int Ticker_Scrolls(void); |
| 2187 | static uint64_t ticker_at; // when it was last drawn (it's redrawn as it moves) |
| 2188 |
|
| 2189 | /* |
| 2190 | ================== |
| 2191 | The intro: the logo eases up and in over the backdrop, a glow passes behind it and a line draws out |
| 2192 | under it, then it all fades into the app (which loads underneath meanwhile). Any button hurries it on |
| 2193 | to the fade. |
| 2194 | ================== |
| 2195 | */ |
| 2196 | static uint64_t intro_t0; // its first frame (0: not drawn yet) |
| 2197 | static int intro_done; |
| 2198 |
|
| 2199 | static float Phase(float t, float a, float b) { return t <= a ? 0 : t >= b ? 1 : (t - a) / (b - a); } |
| 2200 | static float Ease_Out(float t) { return 1 - (1 - t) * (1 - t) * (1 - t); } |
| 2201 |
|
| 2202 | static 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 |
|
| 2207 | static 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 |
|
| 2243 | void 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 |
|
| 2317 | int ui_needs_redraw(void) |
| 2318 | { |
| 2319 | int d = dirty; |
| 2320 | dirty = 0; |
| 2321 | return d; |
| 2322 | } |
| 2323 |
|
| 2324 | void ui_force_redraw(void) |
| 2325 | { |
| 2326 | dirty = 1; |
| 2327 | } |
| 2328 |
|
| 2329 | // the sections, as tabs (what L1/R1 change on the categories screen) |
| 2330 | static 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 |
|
| 2337 | static 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 |
|
| 2379 | static 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) |
| 2390 | static 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 |
| 2404 | static 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 |
|
| 2426 | static int Draw_Strip(void) { return Draw_Strip_At(SCREEN_H - 55); } |
| 2427 |
|
| 2428 | // "3 Nov 2026", or "" when it isn't known |
| 2429 | static 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" |
| 2438 | static 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 |
| 2448 | static 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 | ================== |
| 2459 | The New ticker: the titles in each section's Recently added list, scrolling along the top |
| 2460 | of 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 |
| 2470 | typedef struct { |
| 2471 | char text[128]; |
| 2472 | int w, head; // head: a section's name (cyan, after a wider gap) |
| 2473 | } tick_t; |
| 2474 | static tick_t ticks[TICKER_SEGS]; |
| 2475 | static int num_ticks, ticker_w; // ticker_w 0: nothing new |
| 2476 |
|
| 2477 | static 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 |
|
| 2515 | static 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) |
| 2521 | static 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 |
|
| 2535 | static 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 |
| 2556 | static 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 |
| 2580 | static 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 |
|
| 2600 | static 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 |
|
| 2639 | static 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 |
| 2685 | static struct { char tid[10]; long long when; uint64_t at; } inst[INST_SLOTS]; |
| 2686 | static int inst_next; |
| 2687 |
|
| 2688 | static 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 | } |
| 2702 | static 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 |
| 2707 | static 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 |
| 2720 | static 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) |
| 2742 | static 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 |
| 2754 | static 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 |
|
| 2801 | static 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 |
| 2875 | static 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 | /* |
| 2886 | The detail screen, in two columns: on the left the preview (640x360) and under it |
| 2887 | the download, how it went, or how to use it after; on the right the title, what |
| 2888 | it 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 |
|
| 2895 | static 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) |
| 3008 | static 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 |
|
| 3020 | static 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 |
|
| 3037 | static 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 |
|
| 3054 | static 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 |
| 3085 | static 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 |
| 3092 | static 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 |
|
| 3097 | static 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 |
|
| 3137 | static 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 |
|
| 3170 | static 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 |
|
| 3238 | void 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 |
|
| 3270 | void 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 | } |