// MBMNET Shop - Homebrew für den 3DS, aus der Universal-DB (über den MBMNET-Server). // // Aufgebaut wie der eShop, in MBMNET-Farben: // Startseite: oben großes Vorschaubild der gewählten App, unten Reiter und ein Raster aus Symbolen. // App-Seite: oben die Screenshots (L/R blättert), unten großer Knopf "Kostenlos herunterladen", // Infos und die Beschreibung. #include <3ds.h> #include #include #include #include #include "pics.h" #include "store.h" #define TOP_W 400 #define BOT_W 320 #define SCR_H 240 #define COL_RED C2D_Color32(0xE4, 0x00, 0x2B, 0xFF) #define COL_RED_DARK C2D_Color32(0x9E, 0x00, 0x1E, 0xFF) #define COL_BG C2D_Color32(0x16, 0x19, 0x1F, 0xFF) #define COL_BG2 C2D_Color32(0x22, 0x26, 0x2E, 0xFF) #define COL_CARD C2D_Color32(0x2A, 0x2F, 0x39, 0xFF) #define COL_CARD_HI C2D_Color32(0x3A, 0x41, 0x4E, 0xFF) #define COL_WHITE C2D_Color32(0xFF, 0xFF, 0xFF, 0xFF) #define COL_MUTED C2D_Color32(0x9A, 0xA3, 0xAF, 0xFF) #define COL_GREEN C2D_Color32(0x22, 0xC5, 0x5E, 0xFF) #define COL_AMBER C2D_Color32(0xF5, 0x9E, 0x0B, 0xFF) #define COL_SHADE C2D_Color32(0x10, 0x12, 0x17, 0xD8) #define COL_SHADE_LT C2D_Color32(0x10, 0x12, 0x17, 0x90) // Startseite: Reiter und Symbol-Raster #define TAB_H 26 #define GRID_Y (TAB_H + 4) #define COLS 5 #define CELL_W 64 #define CELL_H 62 #define FULL_ROWS 3 // App-Seite (unterer Bildschirm) #define DESC_Y 156 #define DESC_LINE_H 13 #define DESC_LINES 6 typedef struct { const char *label, *match, *name; } Tab; static const Tab TABS[] = { { "Alle", NULL, "Alle Apps" }, { "Spiele", "game", "Spiele" }, { "Tools", "utility", "Tools" }, { "Apps", "app", "Apps" }, { "Emus", "emulator", "Emulatoren" }, { "Medien", "media", "Medien" }, { "Meine", "*installed", "Meine Apps" }, }; #define TAB_COUNT (int)(sizeof(TABS) / sizeof(TABS[0])) #define SEARCH_W 34 // Lupe rechts in der Reiterleiste #define TAB_W ((float)(BOT_W - SEARCH_W) / TAB_COUNT) static const char *SORT_NAMES[] = { "nach Name", "beliebteste zuerst", "neueste zuerst" }; typedef struct { float x, y, w, h; } Box; static const Box backBox = { 6, 8, 30, 46 }; static const Box actionBox = { 12, 70, 296, 40 }, actionBoxNarrow = { 12, 70, 208, 40 }, removeBox = { 226, 70, 82, 40 }; static const Box kindBox = { 206, 116, 102, 26 }; // Umschalter CIA | 3DSX (links / rechts) // Ist die gewählte Variante (CIA/3DSX) eine andere als die installierte? static bool other_variant(const App *a) { return a->installed && strcmp(a->kind, a->instKind) != 0; } static C2D_TextBuf textBuf, measureBuf; // measureBuf nur zum Messen (wird jedes Mal geleert) static C2D_SpriteSheet sheets[8]; // --- Zeichenhilfen ------------------------------------------------------------ static void text(const char *s, float x, float y, float size, u32 color, u32 flags) { C2D_Text t; C2D_TextParse(&t, textBuf, s); C2D_TextOptimize(&t); C2D_DrawText(&t, C2D_WithColor | flags, x, y, 0.5f, size, size, color); } static float text_width(const char *s, float size) { C2D_Text t; float w = 0; C2D_TextBufClear(measureBuf); C2D_TextParse(&t, measureBuf, s); C2D_TextGetDimensions(&t, size, size, &w, NULL); return w; } // Text auf eine Breite kürzen ("...") static void fit(char *out, size_t len, const char *s, float size, float width) { snprintf(out, len, "%s", s); if (text_width(out, size) <= width) return; size_t n = strlen(out); while (n > 1) { n--; while (n > 0 && (out[n] & 0xC0) == 0x80) n--; // nicht mitten in einem UTF-8-Zeichen snprintf(out + n, len - n, "..."); if (text_width(out, size) <= width) return; } } static void round_rect(float x, float y, float w, float h, float r, u32 color) { if (r > h / 2) r = h / 2; C2D_DrawRectSolid(x + r, y, 0.5f, w - 2 * r, h, color); C2D_DrawRectSolid(x, y + r, 0.5f, w, h - 2 * r, color); C2D_DrawCircleSolid(x + r, y + r, 0.5f, r, color); C2D_DrawCircleSolid(x + w - r, y + r, 0.5f, r, color); C2D_DrawCircleSolid(x + r, y + h - r, 0.5f, r, color); C2D_DrawCircleSolid(x + w - r, y + h - r, 0.5f, r, color); } static bool inside(Box b, touchPosition tp) { return tp.px >= b.x && tp.px < b.x + b.w && tp.py >= b.y && tp.py < b.y + b.h; } // Einkaufswagen (wie das Symbol der App) static void draw_cart(float x, float y, float size) { float s = size / 100.0f, t = 9 * s; round_rect(x, y, size, size, 22 * s, COL_RED); C2D_DrawLine(x + 13 * s, y + 23 * s, COL_WHITE, x + 23 * s, y + 23 * s, COL_WHITE, t, 0.5f); C2D_DrawLine(x + 23 * s, y + 23 * s, COL_WHITE, x + 33 * s, y + 61 * s, COL_WHITE, t, 0.5f); C2D_DrawLine(x + 33 * s, y + 61 * s, COL_WHITE, x + 30 * s, y + 70 * s, COL_WHITE, t, 0.5f); C2D_DrawLine(x + 30 * s, y + 70 * s, COL_WHITE, x + 78 * s, y + 70 * s, COL_WHITE, t, 0.5f); C2D_DrawTriangle(x + 27 * s, y + 33 * s, COL_WHITE, x + 84 * s, y + 33 * s, COL_WHITE, x + 77 * s, y + 61 * s, COL_WHITE, 0.5f); C2D_DrawTriangle(x + 27 * s, y + 33 * s, COL_WHITE, x + 77 * s, y + 61 * s, COL_WHITE, x + 33 * s, y + 61 * s, COL_WHITE, 0.5f); C2D_DrawCircleSolid(x + 37 * s, y + 80 * s, 0.5f, 7 * s, COL_WHITE); C2D_DrawCircleSolid(x + 72 * s, y + 80 * s, 0.5f, 7 * s, COL_WHITE); } static void draw_icon(const App *a, float x, float y, float size) { C2D_SpriteSheet sh = a->sheet >= 0 && a->sheet < 8 ? sheets[a->sheet] : NULL; if (sh && a->icon >= 0 && (size_t)a->icon < C2D_SpriteSheetCount(sh)) { C2D_Image img = C2D_SpriteSheetGetImage(sh, a->icon); float sc = size / (img.subtex->width ? img.subtex->width : 48); C2D_DrawImageAt(img, x, y, 0.5f, NULL, sc, sc); return; } // kein Symbol: Anfangsbuchstabe auf grauem Feld round_rect(x, y, size, size, size / 6, COL_CARD_HI); char first[8] = { 0 }; int n = 1; while (n < 4 && (a->title[n] & 0xC0) == 0x80) n++; memcpy(first, a->title, n); text(first, x + size / 2, y + size * 0.2f, size / 48.0f, COL_WHITE, C2D_AlignCenter); } static void size_text(char *out, size_t len, u32 bytes) { if (bytes >= 1024 * 1024) snprintf(out, len, "%.1f MB", bytes / (1024.0 * 1024.0)); else if (bytes) snprintf(out, len, "%lu KB", (unsigned long)((bytes + 1023) / 1024)); else snprintf(out, len, "?"); } static const char *category_name(const char *c) { if (!strcmp(c, "game")) return "Spiel"; if (!strcmp(c, "emulator")) return "Emulator"; if (!strcmp(c, "media")) return "Medien"; if (!strcmp(c, "save-tool")) return "Spielstände"; if (!strcmp(c, "plugin")) return "Plugin"; if (!strcmp(c, "utility")) return "Tool"; return "App"; } static u32 app_color(const App *a, float factor) { u32 c = a->colorBg ? a->colorBg : 0x3A414E; int r = ((c >> 16) & 255) * factor, g = ((c >> 8) & 255) * factor, b = (c & 255) * factor; return C2D_Color32(r > 255 ? 255 : r, g > 255 ? 255 : g, b > 255 ? 255 : b, 0xFF); } // --- Liste (gefiltert und sortiert) --------------------------------------------- static int *view; // Indizes in den Katalog static int viewCount; static int sortMode; static char search[64]; // Suchbegriff (leer = keine Suche) // Enthält text den Suchbegriff? (Groß-/Kleinschreibung egal) static bool contains(const char *text, const char *needle) { if (!text) return false; size_t n = strlen(needle); for (; *text; text++) if (strncasecmp(text, needle, n) == 0) return true; return false; } static bool matches_search(const App *a) { return !search[0] || contains(a->title, search) || contains(a->author, search) || contains(a->description, search); } static int cmp_name(const void *a, const void *b) { return strcasecmp(catalog_app(*(const int *)a)->title, catalog_app(*(const int *)b)->title); } static int cmp_stars(const void *a, const void *b) { const App *x = catalog_app(*(const int *)a), *y = catalog_app(*(const int *)b); if (x->stars != y->stars) return x->stars > y->stars ? -1 : 1; return strcasecmp(x->title, y->title); } static int cmp_new(const void *a, const void *b) { const App *x = catalog_app(*(const int *)a), *y = catalog_app(*(const int *)b); int c = strcmp(y->updated, x->updated); return c ? c : strcasecmp(x->title, y->title); } static bool in_tab(const App *a, int tab) { const char *m = TABS[tab].match; if (!m) return true; if (strcmp(m, "*installed") == 0) return a->installed; if (strcmp(m, "utility") == 0) return !strcmp(a->category, "utility") || !strcmp(a->category, "save-tool") || !strcmp(a->category, "plugin"); return strcmp(a->category, m) == 0; } static void rebuild_view(int tab) { int n = catalog_count(); free(view); view = malloc(sizeof(int) * (n ? n : 1)); viewCount = 0; for (int i = 0; i < n; i++) if (in_tab(catalog_app(i), tab) && matches_search(catalog_app(i))) view[viewCount++] = i; qsort(view, viewCount, sizeof(int), sortMode == 1 ? cmp_stars : sortMode == 2 ? cmp_new : cmp_name); } // --- Beschreibung in Zeilen umbrechen (einmal beim Öffnen der App-Seite) ---------- #define MAX_LINES 64 static char descLines[MAX_LINES][96]; static int descLineCount; static void wrap_description(const App *a, float size, float width) { descLineCount = 0; const char *p = a->description ? a->description : ""; char line[96] = ""; while (*p && descLineCount < MAX_LINES) { while (*p == ' ') p++; const char *end = p; while (*end && *end != ' ') end++; char candidate[96]; int wordLen = end - p; if (wordLen > 80) wordLen = 80; if (line[0]) snprintf(candidate, sizeof(candidate), "%s %.*s", line, wordLen, p); else snprintf(candidate, sizeof(candidate), "%.*s", wordLen, p); if (line[0] && (strlen(candidate) >= sizeof(line) - 1 || text_width(candidate, size) > width)) { snprintf(descLines[descLineCount++], sizeof(descLines[0]), "%s", line); snprintf(line, sizeof(line), "%.*s", wordLen, p); } else { snprintf(line, sizeof(line), "%s", candidate); } p = end; } if (line[0] && descLineCount < MAX_LINES) snprintf(descLines[descLineCount++], sizeof(descLines[0]), "%s", line); } // --- Oberer Bildschirm -------------------------------------------------------------- // Großes Bild der App: Screenshot/Banner vom Server, sonst ihre Farbe mit großem Symbol static void draw_hero(const App *a, int which) { C2D_Image img; PicState st = PIC_MISSING; if (which >= 0 && which < a->shots) st = pics_get(a->slug, which, &img); else if (a->banner) st = pics_get(a->slug, PIC_BANNER, &img); if (st == PIC_READY) { C2D_DrawImageAt(img, 0, 0, 0.5f, NULL, 1.0f, 1.0f); return; } C2D_DrawRectangle(0, 0, 0.5f, TOP_W, SCR_H, app_color(a, 1.0f), app_color(a, 1.0f), app_color(a, 0.45f), app_color(a, 0.45f)); // Symbol groß, leicht versetzt, mit dezentem Schatten round_rect(TOP_W / 2 - 54, 34, 108, 108, 16, COL_SHADE_LT); draw_icon(a, TOP_W / 2 - 48, 38, 96); if (st == PIC_LOADING) text("Lade Bild ...", TOP_W - 10, 30, 0.38f, COL_WHITE, C2D_AlignRight); } static void draw_top_bar(const char *left, const char *right) { C2D_DrawRectSolid(0, 0, 0.5f, TOP_W, 22, COL_SHADE); draw_cart(6, 3, 16); text(left, 28, 4, 0.45f, COL_WHITE, 0); text(right, TOP_W - 8, 5, 0.38f, COL_MUTED, C2D_AlignRight); } static void draw_price_pill(const App *a, float right, float y) { AppStatus s = app_status(a); const char *label = s == APP_NEW ? "Kostenlos" : s == APP_UPDATE ? "Update" : "Installiert"; u32 col = s == APP_NEW ? COL_RED : s == APP_UPDATE ? COL_AMBER : COL_GREEN; float w = text_width(label, 0.5f) + 22; round_rect(right - w, y, w, 24, 12, col); text(label, right - w / 2, y + 4, 0.5f, s == APP_NEW ? COL_WHITE : COL_BG, C2D_AlignCenter); } // Infozeile: Version, Größe, Art, Kategorie static void info_line(char *out, size_t len, const App *a) { char size[16]; size_text(size, sizeof(size), a->size); snprintf(out, len, "%s · %s · %s · %s", a->version[0] ? a->version : "ohne Version", size, category_name(a->category), strcmp(a->kind, "cia") == 0 ? "HOME-Menü" : "Homebrew Launcher"); } static void draw_toast(const char *msg, u32 color) { if (!msg[0]) return; float w = 0, h = 0; C2D_Text t; C2D_TextBufClear(measureBuf); C2D_TextParse(&t, measureBuf, msg); C2D_TextGetDimensions(&t, 0.46f, 0.46f, &w, &h); if (w > TOP_W - 40) w = TOP_W - 40; round_rect(TOP_W / 2 - w / 2 - 12, 30, w + 24, h + 12, 10, COL_SHADE); C2D_DrawRectSolid(TOP_W / 2 - w / 2 - 12, 36, 0.5f, 3, h, color); text(msg, TOP_W / 2 - w / 2, 36, 0.46f, COL_WHITE, 0); } static void draw_top_home(const App *a, int tab, bool showHero) { char right[64], line[96]; if (!a) { C2D_DrawRectangle(0, 0, 0, TOP_W, SCR_H, COL_BG2, COL_BG2, COL_BG, COL_BG); draw_cart(TOP_W / 2 - 40, 56, 80); const char *m = catalog_state() != CAT_READY ? catalog_message() : search[0] ? "Nichts gefunden. Y: anders suchen, B: Suche beenden" : tab == TAB_COUNT - 1 ? "Noch nichts aus dem Shop installiert." : "Hier gibt es nichts."; text(m[0] ? m : "Einen Moment ...", TOP_W / 2, 160, 0.5f, COL_MUTED, C2D_AlignCenter); text("Homebrew aus der Universal-DB", TOP_W / 2, 184, 0.4f, COL_MUTED, C2D_AlignCenter); draw_top_bar("MBMNET Shop", "v" APP_VERSION); return; } if (showHero) draw_hero(a, 0); else C2D_DrawRectangle(0, 0, 0.5f, TOP_W, SCR_H, app_color(a, 1.0f), app_color(a, 1.0f), app_color(a, 0.45f), app_color(a, 0.45f)); if (search[0]) snprintf(right, sizeof(right), "Suche \"%.20s\" · %d Treffer · B: Ende", search, viewCount); else snprintf(right, sizeof(right), "%s · %d · %s%s", TABS[tab].name, viewCount, SORT_NAMES[sortMode], catalog_offline() ? " · offline" : ""); draw_top_bar("MBMNET Shop", right); // Unten: Symbol, Name, Autor, Infos, Preis-Schild C2D_DrawRectSolid(0, 166, 0.5f, TOP_W, 74, COL_SHADE); C2D_DrawRectSolid(0, 166, 0.5f, TOP_W, 2, COL_RED); draw_icon(a, 10, 178, 48); fit(line, sizeof(line), a->title, 0.62f, TOP_W - 180); text(line, 68, 175, 0.62f, COL_WHITE, 0); fit(line, sizeof(line), a->author, 0.42f, TOP_W - 180); text(line, 68, 196, 0.42f, COL_MUTED, 0); info_line(line, sizeof(line), a); char cut[96]; fit(cut, sizeof(cut), line, 0.36f, TOP_W - 80); text(cut, 68, 214, 0.36f, COL_MUTED, 0); draw_price_pill(a, TOP_W - 10, 182); } static void draw_top_detail(const App *a, int shot) { draw_hero(a, a->shots ? shot : -1); char right[48]; snprintf(right, sizeof(right), "%s%sB: Zurück", a->shots > 1 ? "L/R: Bilder " : "", app_has_both(a) ? "X: CIA/3DSX " : ""); draw_top_bar(a->title, right); // Punkte für die Screenshots if (a->shots > 1) { float w = a->shots * 12; round_rect(TOP_W / 2 - w / 2 - 6, 222, w + 12, 14, 7, COL_SHADE); for (int i = 0; i < a->shots; i++) C2D_DrawCircleSolid(TOP_W / 2 - w / 2 + 6 + i * 12, 229, 0.5f, i == shot ? 4 : 2.5f, i == shot ? COL_RED : COL_WHITE); } } // --- Unterer Bildschirm ------------------------------------------------------------ static void draw_tabs(int tab) { float w = TAB_W; C2D_DrawRectSolid(0, 0, 0.5f, BOT_W, TAB_H, COL_BG2); // Lupe (Suche); rot hinterlegt, solange gesucht wird float lx = BOT_W - SEARCH_W; if (search[0]) C2D_DrawRectSolid(lx, 0, 0.5f, SEARCH_W, TAB_H, COL_RED); C2D_DrawCircleSolid(lx + 15, 11, 0.5f, 6.5f, COL_WHITE); C2D_DrawCircleSolid(lx + 15, 11, 0.5f, 4.5f, search[0] ? COL_RED : COL_BG2); C2D_DrawLine(lx + 19, 15, COL_WHITE, lx + 25, 21, COL_WHITE, 3, 0.5f); for (int i = 0; i < TAB_COUNT; i++) { bool on = i == tab; if (on) { C2D_DrawRectSolid(i * w, 0, 0.5f, w, TAB_H, COL_CARD_HI); C2D_DrawRectSolid(i * w + 6, TAB_H - 3, 0.5f, w - 12, 3, COL_RED); } text(TABS[i].label, i * w + w / 2, 7, 0.38f, on ? COL_WHITE : COL_MUTED, C2D_AlignCenter); } } static void draw_bottom_home(int tab, int selected, int scrollRow) { C2D_DrawRectSolid(0, 0, 0, BOT_W, SCR_H, COL_BG); draw_tabs(tab); if (catalog_state() != CAT_READY) { const char *m = catalog_state() == CAT_ERROR ? "Kein Katalog.\nA: nochmal versuchen" : "Einen Moment ..."; text(m, BOT_W / 2, 110, 0.5f, COL_MUTED, C2D_AlignCenter); return; } if (!viewCount) return; for (int r = 0; r <= FULL_ROWS; r++) { for (int c = 0; c < COLS; c++) { int i = (scrollRow + r) * COLS + c; if (i >= viewCount) break; const App *a = catalog_app(view[i]); float x = c * CELL_W + 5, y = GRID_Y + r * CELL_H; if (i == selected) round_rect(x - 3, y - 3, 60, 60, 12, COL_RED); round_rect(x, y, 54, 54, 10, i == selected ? COL_CARD_HI : COL_CARD); draw_icon(a, x + 3, y + 3, 48); AppStatus s = app_status(a); if (s != APP_NEW) { C2D_DrawCircleSolid(x + 50, y + 4, 0.5f, 6, COL_BG); C2D_DrawCircleSolid(x + 50, y + 4, 0.5f, 4.5f, s == APP_UPDATE ? COL_AMBER : COL_GREEN); } } } // Rollbalken int rows = (viewCount + COLS - 1) / COLS; if (rows > FULL_ROWS) { float h = SCR_H - GRID_Y - 4, bar = h * FULL_ROWS / rows; float y = GRID_Y + (h - bar) * scrollRow / (rows - FULL_ROWS); round_rect(BOT_W - 4, y, 3, bar, 1.5f, COL_MUTED); } } static void draw_bottom_detail(const App *a, int armed, int descScroll) { C2D_DrawRectSolid(0, 0, 0, BOT_W, SCR_H, COL_BG); // Kopf: Zurück, Symbol, Name, Autor C2D_DrawRectSolid(0, 0, 0.5f, BOT_W, 62, COL_BG2); round_rect(backBox.x, backBox.y + 8, backBox.w, 30, 8, COL_CARD); C2D_DrawLine(backBox.x + 18, backBox.y + 15, COL_WHITE, backBox.x + 11, backBox.y + 23, COL_WHITE, 3, 0.5f); C2D_DrawLine(backBox.x + 11, backBox.y + 23, COL_WHITE, backBox.x + 18, backBox.y + 31, COL_WHITE, 3, 0.5f); draw_icon(a, 44, 7, 48); char line[128]; fit(line, sizeof(line), a->title, 0.6f, BOT_W - 108); text(line, 100, 9, 0.6f, COL_WHITE, 0); fit(line, sizeof(line), a->author, 0.42f, BOT_W - 108); text(line, 100, 32, 0.42f, COL_MUTED, 0); // Großer Knopf (wie "Kostenlos herunterladen" im eShop) und Entfernen AppStatus s = app_status(a); bool running = job_state() == JOB_RUNNING; Box b = s == APP_NEW ? actionBox : actionBoxNarrow; bool switchKind = other_variant(a); const char *label = s == APP_NEW ? "Kostenlos herunterladen" : switchKind ? (strcmp(a->kind, "cia") == 0 ? "Als CIA installieren" : "Als 3DSX installieren") : s == APP_UPDATE ? "Aktualisieren" : strcmp(a->kind, "cia") == 0 ? "Öffnen" : "Neu laden"; if (switchKind) s = APP_UPDATE; // wie ein Update: roter Knopf, Bestätigung u32 col = s == APP_INSTALLED ? COL_GREEN : COL_RED; if (armed == 0) { label = s == APP_NEW ? "Nochmal tippen: Laden" : "Nochmal tippen"; col = COL_AMBER; } char progress[48]; if (running) { float p = job_progress(); round_rect(b.x, b.y, b.w, b.h, 12, COL_CARD_HI); if (p > 0) round_rect(b.x, b.y, 24 + (b.w - 24) * (p > 1 ? 1 : p), b.h, 12, COL_RED); if (p >= 0.999f) snprintf(progress, sizeof(progress), "Wird installiert ..."); else if (p >= 0) snprintf(progress, sizeof(progress), "Lädt ... %d %%", (int)(p * 100)); else snprintf(progress, sizeof(progress), "Lädt ..."); label = progress; } else { round_rect(b.x, b.y + 3, b.w, b.h, 12, s == APP_INSTALLED ? C2D_Color32(0x15, 0x80, 0x3D, 0xFF) : COL_RED_DARK); round_rect(b.x, b.y, b.w, b.h, 12, col); } text(label, b.x + b.w / 2, b.y + 10, 0.55f, s == APP_INSTALLED && !running && armed != 0 ? COL_BG : COL_WHITE, C2D_AlignCenter); if (s != APP_NEW) { Box r = removeBox; round_rect(r.x, r.y, r.w, r.h, 12, armed == 1 ? COL_AMBER : COL_CARD); text(armed == 1 ? "Sicher?" : "Entfernen", r.x + r.w / 2, r.y + 12, 0.45f, armed == 1 ? COL_BG : COL_MUTED, C2D_AlignCenter); } // Infos char size[16]; size_text(size, sizeof(size), a->size); char info1[96]; snprintf(info1, sizeof(info1), "%s · %s · %s", a->version[0] ? a->version : "ohne Version", size, category_name(a->category)); bool both = app_has_both(a); fit(line, sizeof(line), info1, 0.42f, both ? kindBox.x - 22 : BOT_W - 28); text(line, 14, 118, 0.42f, COL_WHITE, 0); // Umschalter CIA | 3DSX (nur wenn es beide gibt) if (both) { Box k = kindBox; bool cia = strcmp(a->kind, "cia") == 0; round_rect(k.x, k.y, k.w, k.h, k.h / 2, COL_CARD); round_rect(cia ? k.x + 2 : k.x + k.w / 2, k.y + 2, k.w / 2 - 2, k.h - 4, (k.h - 4) / 2, COL_RED); text("CIA", k.x + k.w / 4, k.y + 5, 0.42f, cia ? COL_WHITE : COL_MUTED, C2D_AlignCenter); text("3DSX", k.x + k.w * 3 / 4, k.y + 5, 0.42f, cia ? COL_MUTED : COL_WHITE, C2D_AlignCenter); } char info2[128]; snprintf(info2, sizeof(info2), "%s%s%s%s", strcmp(a->kind, "cia") == 0 ? "Im HOME-Menü" : "Im Homebrew Launcher", a->updated[0] ? " · aktualisiert " : "", a->updated, a->license[0] ? " · " : ""); if (a->license[0]) snprintf(info2 + strlen(info2), sizeof(info2) - strlen(info2), "%s", a->license); fit(line, sizeof(line), info2, 0.38f, both ? kindBox.x - 22 : BOT_W - 28); text(line, 14, 134, 0.38f, COL_MUTED, 0); C2D_DrawRectSolid(12, 150, 0.5f, BOT_W - 24, 1, COL_CARD_HI); // Beschreibung for (int i = 0; i < DESC_LINES && descScroll + i < descLineCount; i++) text(descLines[descScroll + i], 14, DESC_Y + i * DESC_LINE_H, 0.42f, COL_WHITE, 0); if (descLineCount > DESC_LINES) { float h = DESC_LINES * DESC_LINE_H, bar = h * DESC_LINES / descLineCount; float y = DESC_Y + (h - bar) * descScroll / (descLineCount - DESC_LINES); round_rect(BOT_W - 6, y, 3, bar, 1.5f, COL_MUTED); } } // --- Aktionen -------------------------------------------------------------------- // Installierte CIA starten (Sprung in die App, der Shop beendet sich) static bool launch(const App *a, char *msg, size_t len) { if (!a->titleId) { snprintf(msg, len, "Starte %s bitte im HOME-Menü.", a->title); return false; } Result rc = APT_PrepareToDoApplicationJump(0, a->titleId, MEDIATYPE_SD); if (R_SUCCEEDED(rc)) { static u8 param[0x300], hmac[0x20]; rc = APT_DoApplicationJump(param, sizeof(param), hmac); } if (R_FAILED(rc)) { snprintf(msg, len, "Starte %s bitte im HOME-Menü (%08lX).", a->title, (unsigned long)rc); return false; } return true; } static void load_sheets(void) { for (int n = 0; n < catalog_sheet_count() && n < 8; n++) { const char *p = catalog_sheet_path(n); if (p[0] && !sheets[n]) sheets[n] = C2D_SpriteSheetLoad(p); } } int main(int argc, char **argv) { (void)argc; (void)argv; gfxInitDefault(); httpcInit(0); amInit(); C3D_Init(C3D_DEFAULT_CMDBUF_SIZE); C2D_Init(C2D_DEFAULT_MAX_OBJECTS); C2D_Prepare(); C3D_RenderTarget *top = C2D_CreateScreenTarget(GFX_TOP, GFX_LEFT); C3D_RenderTarget *bot = C2D_CreateScreenTarget(GFX_BOTTOM, GFX_LEFT); textBuf = C2D_TextBufNew(8192); measureBuf = C2D_TextBufNew(512); hidSetRepeatParameters(18, 4); pics_init(); catalog_start(); int tab = 0, selected = 0, scrollRow = 0; bool ready = false, detail = false, quit = false; int shot = 0, descScroll = 0, settle = 0; // settle: Bilder erst laden, wenn die Auswahl kurz stehen bleibt char toast[192] = ""; u32 toastColor = COL_RED; int toastFrames = 0; int armed = -1; // Sicherheitsabfrage: 0 = laden, 1 = entfernen int touchStartY = -1, touchStartScroll = 0, touchCell = -1; bool dragged = false; while (!quit && aptMainLoop()) { hidScanInput(); u32 down = hidKeysDown(), rep = hidKeysDownRepeat(), held = hidKeysHeld(); bool busy = job_state() == JOB_RUNNING; if (down & KEY_START && !busy) break; pics_poll(); if (!ready && catalog_state() == CAT_READY) { load_sheets(); rebuild_view(tab); ready = true; } if (catalog_state() == CAT_ERROR && down & KEY_A) catalog_start(); // Ergebnis einer Installation JobState js = job_state(); if (js == JOB_DONE || js == JOB_FAILED) { snprintf(toast, sizeof(toast), "%s", job_message()); toastColor = js == JOB_DONE ? COL_GREEN : COL_AMBER; toastFrames = 300; job_acknowledge(); const App *cur = viewCount ? catalog_app(view[selected]) : NULL; rebuild_view(tab); selected = 0; for (int i = 0; cur && i < viewCount; i++) if (catalog_app(view[i]) == cur) selected = i; if (!viewCount) detail = false; } App *a = ready && viewCount ? catalog_app(view[selected]) : NULL; touchPosition tp; if (ready && !detail) { // --- Startseite --- int oldSel = selected, oldTab = tab; if (down & KEY_R) tab = (tab + 1) % TAB_COUNT; if (down & KEY_L) tab = (tab + TAB_COUNT - 1) % TAB_COUNT; if (down & KEY_X) { sortMode = (sortMode + 1) % 3; rebuild_view(tab); selected = scrollRow = 0; } if (rep & (KEY_RIGHT | KEY_CPAD_RIGHT)) selected++; if (rep & (KEY_LEFT | KEY_CPAD_LEFT)) selected--; if (rep & (KEY_DOWN | KEY_CPAD_DOWN) && selected + COLS < viewCount) selected += COLS; if (rep & (KEY_UP | KEY_CPAD_UP) && selected >= COLS) selected -= COLS; // Touch: Reiter antippen, Raster ziehen, Symbol antippen = App öffnen int rows = (viewCount + COLS - 1) / COLS, maxRow = rows > FULL_ROWS ? rows - FULL_ROWS : 0; bool openSearch = down & KEY_Y; if (down & KEY_TOUCH) { hidTouchRead(&tp); if (tp.py < TAB_H && tp.px >= BOT_W - SEARCH_W) openSearch = true; else if (tp.py < TAB_H) tab = tp.px / TAB_W; else { touchStartY = tp.py; touchStartScroll = scrollRow; dragged = false; int c = tp.px / CELL_W, r = (tp.py - GRID_Y) / CELL_H; touchCell = tp.py >= GRID_Y && c < COLS ? (scrollRow + r) * COLS + c : -1; } } else if (held & KEY_TOUCH && touchStartY >= 0) { hidTouchRead(&tp); int dy = touchStartY - tp.py; if (abs(dy) > 8) dragged = true; if (dragged) { scrollRow = touchStartScroll + (dy + (dy > 0 ? CELL_H / 2 : -CELL_H / 2)) / CELL_H; if (scrollRow > maxRow) scrollRow = maxRow; if (scrollRow < 0) scrollRow = 0; // Auswahl im sichtbaren Bereich halten if (selected < scrollRow * COLS) selected = scrollRow * COLS; if (selected >= (scrollRow + FULL_ROWS) * COLS) selected = (scrollRow + FULL_ROWS) * COLS - 1; } } else if (touchStartY >= 0) { if (!dragged && touchCell >= 0 && touchCell < viewCount) { selected = touchCell; down |= KEY_A; } touchStartY = -1; } // Suche: Bildschirmtastatur des 3DS. Gesucht wird in Name, Autor und Beschreibung, in allen Kategorien. if (openSearch) { SwkbdState kb; char input[sizeof(search)]; swkbdInit(&kb, SWKBD_TYPE_NORMAL, 2, sizeof(search) - 1); swkbdSetHintText(&kb, "Suchen: Name, Autor oder Stichwort"); swkbdSetInitialText(&kb, search); swkbdSetButton(&kb, SWKBD_BUTTON_LEFT, "Abbrechen", false); swkbdSetButton(&kb, SWKBD_BUTTON_RIGHT, "Suchen", true); if (swkbdInputText(&kb, input, sizeof(input)) == SWKBD_BUTTON_RIGHT) { snprintf(search, sizeof(search), "%s", input); tab = oldTab = 0; rebuild_view(tab); selected = scrollRow = 0; settle = 0; } } else if (down & KEY_B && search[0]) { search[0] = 0; rebuild_view(tab); selected = scrollRow = 0; } if (tab != oldTab) { rebuild_view(tab); selected = scrollRow = 0; } if (selected >= viewCount) selected = viewCount - 1; if (selected < 0) selected = 0; if (!dragged || touchStartY < 0) { int row = selected / COLS; if (row < scrollRow) scrollRow = row; if (row >= scrollRow + FULL_ROWS) scrollRow = row - FULL_ROWS + 1; } if (selected != oldSel || tab != oldTab) settle = 0; a = viewCount ? catalog_app(view[selected]) : NULL; if (a && down & KEY_A) { detail = true; shot = descScroll = 0; armed = -1; wrap_description(a, 0.42f, BOT_W - 30); } } else if (ready && a) { // --- App-Seite --- AppStatus s = app_status(a); bool switchKind = other_variant(a); if (switchKind) s = APP_UPDATE; // andere Variante gewählt: Knopf installiert sie bool hit = false, hitRemove = false; if (down & KEY_TOUCH) { hidTouchRead(&tp); touchStartY = tp.py; touchStartScroll = descScroll; dragged = false; if (inside(backBox, tp)) down |= KEY_B; else if (app_has_both(a) && !busy && inside(kindBox, tp)) { app_choose(a, tp.px < kindBox.x + kindBox.w / 2 ? "cia" : "3dsx", true); armed = -1; } else if (inside(s == APP_NEW ? actionBox : actionBoxNarrow, tp)) hit = true; else if (s != APP_NEW && inside(removeBox, tp)) hitRemove = true; } else if (held & KEY_TOUCH && touchStartY >= DESC_Y - 10) { hidTouchRead(&tp); descScroll = touchStartScroll + (touchStartY - tp.py) / DESC_LINE_H; } else touchStartY = -1; if (rep & (KEY_DOWN | KEY_CPAD_DOWN)) descScroll++; if (rep & (KEY_UP | KEY_CPAD_UP)) descScroll--; int maxScroll = descLineCount > DESC_LINES ? descLineCount - DESC_LINES : 0; if (descScroll > maxScroll) descScroll = maxScroll; if (descScroll < 0) descScroll = 0; if (a->shots > 1 && down & KEY_R) shot = (shot + 1) % a->shots; if (a->shots > 1 && down & KEY_L) shot = (shot + a->shots - 1) % a->shots; if (down & KEY_X && app_has_both(a) && !busy) { app_choose(a, strcmp(a->kind, "cia") == 0 ? "3dsx" : "cia", true); armed = -1; } if (down & KEY_B && !busy) { detail = false; armed = -1; } else if (!busy && (down & KEY_A || hit)) { if (s == APP_INSTALLED && strcmp(a->kind, "cia") == 0) { armed = -1; if (launch(a, toast, sizeof(toast))) quit = true; else { toastColor = COL_AMBER; toastFrames = 240; } } else if (armed == 0) { armed = -1; job_install(a); } else if (catalog_offline()) { snprintf(toast, sizeof(toast), "Keine Verbindung zum MBMNET-Server."); toastColor = COL_AMBER; toastFrames = 240; } else armed = 0; } else if (!busy && s != APP_NEW && (down & KEY_Y || hitRemove)) { if (armed == 1) { armed = -1; job_remove(a); } else armed = 1; } } if (toastFrames > 0 && --toastFrames == 0) toast[0] = 0; if (settle < 1000) settle++; const App *cur = ready && viewCount ? catalog_app(view[selected]) : NULL; C2D_TextBufClear(textBuf); C3D_FrameBegin(C3D_FRAME_SYNCDRAW); C2D_TargetClear(top, COL_BG); C2D_SceneBegin(top); if (detail && cur) draw_top_detail(cur, shot); else draw_top_home(cur, tab, settle > 12); draw_toast(toast, toastColor); C2D_TargetClear(bot, COL_BG); C2D_SceneBegin(bot); if (detail && cur) draw_bottom_detail(cur, armed, descScroll); else draw_bottom_home(tab, selected, scrollRow); C3D_FrameEnd(0); } store_exit(); pics_exit(); for (int n = 0; n < 8; n++) if (sheets[n]) C2D_SpriteSheetFree(sheets[n]); free(view); C2D_TextBufDelete(textBuf); C2D_TextBufDelete(measureBuf); C2D_Fini(); C3D_Fini(); amExit(); httpcExit(); gfxExit(); return 0; }