diff --git a/Makefile b/Makefile index 9e17adb..a0cc21e 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ SOURCES := source INCLUDES := source # Bei jedem Release erhöhen - das Auto-Update vergleicht diese Nummer -VERSION := 2.9.0 +VERSION := 2.9.1 APP_TITLE := MBMNET APP_DESCRIPTION := Dein eigenes 3DS-Netzwerk (v$(VERSION)) diff --git a/gm9/mbmnet-installieren.gm9 b/gm9/mbmnet-installieren.gm9 new file mode 100644 index 0000000..128d8ab --- /dev/null +++ b/gm9/mbmnet-installieren.gm9 @@ -0,0 +1,45 @@ +# MBMNET installieren - GodMode9-Skript +# +# Installiert die MBMNET-App von der SD-Karte und auf Wunsch den MBMNET Shop. +# +# So geht's: +# 1. mbmnet.cia (und wer den Shop will: mbmnet-shop.cia) herunterladen: +# https://git.mbm.wtf/MrMaho/MBMNET/releases/latest +# und auf die SD-Karte nach /cias/ kopieren +# 2. Dieses Skript nach /gm9/scripts/ auf der SD-Karte kopieren +# 3. GodMode9 starten (beim Einschalten START gedrueckt halten) +# HOME-Taste > "Scripts..." > "mbmnet-installieren" +# 4. Danach die MBMNET-App im HOME-Menue starten und dort auf "MBMNET" tippen + +set ERRORMSG "MBMNET wurde nicht installiert." +set SUCCESSMSG "Fertig!\n \nStarte jetzt die MBMNET-App im HOME-Menue\nund tippe dort auf MBMNET." + +ask "MBMNET installieren?\n \nDas Skript installiert die MBMNET-App\n(mbmnet.cia) von der SD-Karte." +set ERRORMSG "MBMNET wurde nicht installiert.\n \nLiegt mbmnet.cia auf der SD-Karte?" + +# MBMNET-App suchen (sonst selbst auswaehlen lassen) +if find 0:/cias/mbmnet.cia CIA +elif find 0:/mbmnet.cia CIA +elif find 0:/gm9/scripts/mbmnet.cia CIA +elif find 0:/gm9/in/mbmnet.cia CIA +elif find 0:/Download/mbmnet.cia CIA +else + filesel "mbmnet.cia nicht gefunden.\nBitte die MBMNET-CIA auswaehlen:" 0:/*.cia CIA +end + +echo "Installiere:\n$[CIA]" +install "$[CIA]" + +# MBMNET Shop (Homebrew aus der Universal-DB), nur wenn die Datei da ist und man ihn will +if find 0:/cias/mbmnet-shop.cia SHOP +elif find 0:/mbmnet-shop.cia SHOP +elif find 0:/gm9/scripts/mbmnet-shop.cia SHOP +elif find 0:/Download/mbmnet-shop.cia SHOP +else + set SHOP "" +end +if chk -u "$[SHOP]" "" + if ask "Auch den MBMNET Shop installieren?\n \n(Homebrew-Spiele und -Apps\naus der Universal-DB)" + install "$[SHOP]" + end +end diff --git a/release.sh b/release.sh index dd578c1..afdfe0d 100755 --- a/release.sh +++ b/release.sh @@ -27,10 +27,10 @@ if [ "$NUR_RELEASE" = 0 ]; then docker run --rm -v "$PWD":/app -w /app mbmnet-3ds-build bash -c \ 'make clean >/dev/null && make && make cia && make -C shop clean >/dev/null && make -C shop && make -C shop cia' - cp mbmnet.3dsx mbmnet.cia shop/mbmnet-shop.3dsx shop/mbmnet-shop.cia "$RELEASE/" + cp mbmnet.3dsx mbmnet.cia shop/mbmnet-shop.3dsx shop/mbmnet-shop.cia gm9/mbmnet-installieren.gm9 "$RELEASE/" echo "$VERSION" > "$RELEASE/version.txt" - (cd "$RELEASE" && git add mbmnet.3dsx mbmnet.cia mbmnet-shop.3dsx mbmnet-shop.cia version.txt && git commit -m "MBMNET-App v$VERSION" && git push) + (cd "$RELEASE" && git add mbmnet.3dsx mbmnet.cia mbmnet-shop.3dsx mbmnet-shop.cia mbmnet-installieren.gm9 version.txt && git commit -m "MBMNET-App v$VERSION" && git push) fi # --- Gitea-Release ------------------------------------------------------------------- @@ -57,7 +57,7 @@ else fi # Vorhandene Dateien gleichen Namens löschen (bei erneutem Lauf), dann hochladen -for f in mbmnet.cia mbmnet.3dsx mbmnet-shop.cia mbmnet-shop.3dsx; do +for f in mbmnet.cia mbmnet.3dsx mbmnet-shop.cia mbmnet-shop.3dsx mbmnet-installieren.gm9; do [ -f "$RELEASE/$f" ] || continue for old in $(api GET "/releases/$ID/assets" | json "' '.join(str(a['id']) for a in d if a['name']=='$f')"); do api DELETE "/releases/$ID/assets/$old" >/dev/null diff --git a/shop/source/main.c b/shop/source/main.c index 5adace7..6c8dd66 100644 --- a/shop/source/main.c +++ b/shop/source/main.c @@ -1,13 +1,16 @@ // MBMNET Shop - Homebrew für den 3DS, aus der Universal-DB (über den MBMNET-Server). // -// Oben: die gewählte App (Symbol, Name, Beschreibung). Unten: Reiter (Kategorien) und die Liste. -// A installiert / aktualisiert / startet, Y entfernt, L/R wechselt die Kategorie, X die Sortierung. +// 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 @@ -24,20 +27,36 @@ #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 ROW_H 40 -#define LIST_Y (TAB_H + 2) -#define VISIBLE_ROWS ((SCR_H - LIST_Y) / ROW_H + 1) +#define GRID_Y (TAB_H + 4) +#define COLS 5 +#define CELL_W 64 +#define CELL_H 62 +#define FULL_ROWS 3 -// Kategorien (Universal-DB) und ihre Reiter -typedef struct { const char *label, *match; } Tab; +// 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 }, { "Spiele", "game" }, { "Tools", "utility" }, { "Apps", "app" }, - { "Emus", "emulator" }, { "Medien", "media" }, { "Meine", "*installed" }, + { "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])) -static const char *SORT_NAMES[] = { "Name", "Beliebt", "Neu" }; +#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 C2D_TextBuf textBuf, measureBuf; // measureBuf nur zum Messen (wird jedes Mal geleert) static C2D_SpriteSheet sheets[8]; @@ -52,14 +71,6 @@ static void text(const char *s, float x, float y, float size, u32 color, u32 fla C2D_DrawText(&t, C2D_WithColor | flags, x, y, 0.5f, size, size, color); } -static void text_wrap(const char *s, float x, float y, float size, u32 color, float width) -{ - C2D_Text t; - C2D_TextParse(&t, textBuf, s); - C2D_TextOptimize(&t); - C2D_DrawText(&t, C2D_WithColor | C2D_WordWrap, x, y, 0.5f, size, size, color, width); -} - static float text_width(const char *s, float size) { C2D_Text t; @@ -86,6 +97,7 @@ static void fit(char *out, size_t len, const char *s, float size, float width) 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); @@ -94,13 +106,13 @@ static void round_rect(float x, float y, float w, float h, float r, u32 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, u32 bg) +static void draw_cart(float x, float y, float size) { - float s = size / 100.0f; - round_rect(x, y, size, size, 22 * s, COL_RED_DARK); - round_rect(x, y, size, size - 4 * s, 22 * s, COL_RED); - float t = 8 * s; + 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); @@ -109,7 +121,6 @@ static void draw_cart(float x, float y, float size, u32 bg) 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); - (void)bg; } static void draw_icon(const App *a, float x, float y, float size) @@ -137,11 +148,45 @@ static void size_text(char *out, size_t len, u32 bytes) 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) { @@ -177,131 +222,274 @@ static void rebuild_view(int tab) view = malloc(sizeof(int) * (n ? n : 1)); viewCount = 0; for (int i = 0; i < n; i++) - if (in_tab(catalog_app(i), tab)) view[viewCount++] = 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); } -// --- Bildschirme --------------------------------------------------------------- +// --- Beschreibung in Zeilen umbrechen (einmal beim Öffnen der App-Seite) ---------- -static void draw_status_badge(const App *a, float right, float y) +#define MAX_LINES 64 +static char descLines[MAX_LINES][96]; +static int descLineCount; + +static void wrap_description(const App *a, float size, float width) { - AppStatus s = app_status(a); - if (s == APP_NEW) return; - const char *label = s == APP_UPDATE ? "Update" : "Installiert"; - u32 col = s == APP_UPDATE ? COL_AMBER : COL_GREEN; - float w = text_width(label, 0.38f) + 10; - round_rect(right - w, y, w, 15, 7, col); - text(label, right - w / 2, y + 1.5f, 0.38f, COL_BG, C2D_AlignCenter); + 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); } -static void draw_top(const App *a, int tab, const char *msg, bool armed) -{ - C2D_DrawRectangle(0, 0, 0, TOP_W, SCR_H, COL_BG2, COL_BG2, COL_BG, COL_BG); - // Kopfzeile - draw_cart(10, 6, 22, COL_BG2); - text("MBMNET Shop", 38, 8, 0.55f, COL_WHITE, 0); - char info[64]; - if (catalog_state() == CAT_READY) - snprintf(info, sizeof(info), "%s · %d Apps · %s%s", TABS[tab].label, viewCount, SORT_NAMES[sortMode], - catalog_offline() ? " · offline" : ""); - else snprintf(info, sizeof(info), "v" APP_VERSION); - text(info, TOP_W - 10, 10, 0.42f, COL_MUTED, C2D_AlignRight); - C2D_DrawRectSolid(0, 32, 0.5f, TOP_W, 2, COL_RED); +// --- Oberer Bildschirm -------------------------------------------------------------- - if (!a) { - draw_cart(TOP_W / 2 - 36, 60, 72, COL_BG2); - const char *m = catalog_state() == CAT_READY ? (tab == TAB_COUNT - 1 ? "Noch nichts aus dem Shop installiert." - : "Hier gibt es nichts.") - : catalog_message(); - text_wrap(m, 40, 150, 0.5f, COL_MUTED, TOP_W - 80); +// 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; } - - // App-Kopf: Symbol, Name, Autor, Version, Größe - round_rect(10, 42, 56, 56, 8, COL_CARD); - draw_icon(a, 14, 46, 48); - char line[96], size[16]; - fit(line, sizeof(line), a->title, 0.7f, TOP_W - 90); - text(line, 76, 42, 0.7f, COL_WHITE, 0); - fit(line, sizeof(line), a->author, 0.45f, TOP_W - 90); - text(line, 76, 64, 0.45f, COL_MUTED, 0); - size_text(size, sizeof(size), a->size); - snprintf(line, sizeof(line), "%s · %s · %s", a->version[0] ? a->version : "ohne Version", - size, strcmp(a->kind, "cia") == 0 ? "CIA" : "3DSX"); - text(line, 76, 82, 0.42f, COL_MUTED, 0); - draw_status_badge(a, TOP_W - 10, 82); - - // Beschreibung oder Meldung - round_rect(10, 106, TOP_W - 20, 104, 8, COL_CARD); - if (msg[0]) { - text_wrap(msg, 20, 114, 0.48f, armed ? COL_AMBER : COL_WHITE, TOP_W - 40); - if (job_state() == JOB_RUNNING) { - float p = job_progress(); - round_rect(20, 184, TOP_W - 40, 14, 7, COL_BG); - if (p >= 0) round_rect(20, 184, 14 + (TOP_W - 54) * (p > 1 ? 1 : p), 14, 7, COL_RED); - } - } else { - char desc[440]; - snprintf(desc, sizeof(desc), "%s", a->description ? a->description : ""); - if (strlen(desc) >= sizeof(desc) - 1) { - char *cut = strrchr(desc, ' '); - if (cut) snprintf(cut, sizeof(desc) - (cut - desc), " ..."); - } - text_wrap(desc, 20, 114, 0.42f, COL_WHITE, TOP_W - 40); - } - - // Tastenhilfe - AppStatus s = app_status(a); - const char *aLabel = s == APP_NEW ? "A: Installieren" : s == APP_UPDATE ? "A: Aktualisieren" - : strcmp(a->kind, "cia") == 0 ? "A: Starten" : "A: Neu laden"; - snprintf(line, sizeof(line), "%s%s L/R: Kategorie X: Sortierung START: Ende", aLabel, - s != APP_NEW ? " Y: Entfernen" : ""); - text(line, TOP_W / 2, 220, 0.38f, COL_MUTED, C2D_AlignCenter); + 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_bottom(int tab, int selected, int scroll) +static void draw_top_bar(const char *left, const char *right) { - C2D_DrawRectSolid(0, 0, 0, BOT_W, SCR_H, COL_BG); - // Reiter - float w = (float)BOT_W / TAB_COUNT; + 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), a->shots > 1 ? "L / R: Bilder B: Zurück" : "B: Zurück"); + 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; - C2D_DrawRectSolid(i * w, 0, 0.5f, w, TAB_H, on ? COL_RED : COL_BG2); - text(TABS[i].label, i * w + w / 2, 6, 0.42f, on ? COL_WHITE : COL_MUTED, C2D_AlignCenter); + 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) { - text_wrap(catalog_state() == CAT_ERROR ? "Kein Katalog. A: nochmal versuchen" : "Einen Moment ...", - 20, 110, 0.5f, COL_MUTED, BOT_W - 40); + 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; } - // Liste - for (int r = 0; r < VISIBLE_ROWS; r++) { - int i = scroll + r; - if (i >= viewCount) break; - const App *a = catalog_app(view[i]); - float y = LIST_Y + r * ROW_H; - if (i == selected) { - C2D_DrawRectSolid(0, y, 0.5f, BOT_W, ROW_H, COL_CARD_HI); - C2D_DrawRectSolid(0, y + 6, 0.5f, 4, ROW_H - 12, COL_RED); - } else if (i % 2) { - C2D_DrawRectSolid(0, y, 0.5f, BOT_W, ROW_H, COL_BG2); + 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); + } } - draw_icon(a, 8, y + 4, 32); - char line[96]; - AppStatus s = app_status(a); - float right = s == APP_NEW ? BOT_W - 8 : BOT_W - 78; - fit(line, sizeof(line), a->title, 0.5f, right - 48); - text(line, 48, y + 4, 0.5f, COL_WHITE, 0); - fit(line, sizeof(line), a->author, 0.38f, right - 48); - text(line, 48, y + 22, 0.38f, COL_MUTED, 0); - draw_status_badge(a, BOT_W - 8, y + 12); } // Rollbalken - if (viewCount > VISIBLE_ROWS - 1) { - float h = SCR_H - LIST_Y, bar = h * (VISIBLE_ROWS - 1) / viewCount; - if (bar < 12) bar = 12; - float y = LIST_Y + (h - bar) * scroll / (viewCount - (VISIBLE_ROWS - 1)); - C2D_DrawRectSolid(BOT_W - 3, y, 0.5f, 3, bar, COL_MUTED); + 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; + const char *label = s == APP_NEW ? "Kostenlos herunterladen" : s == APP_UPDATE ? "Aktualisieren" + : strcmp(a->kind, "cia") == 0 ? "Öffnen" : "Neu laden"; + 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); + snprintf(line, sizeof(line), "%s · %s · %s", a->version[0] ? a->version : "ohne Version", size, category_name(a->category)); + text(line, 14, 118, 0.42f, COL_WHITE, 0); + 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, 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); } } @@ -313,7 +501,7 @@ 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)) { - u8 param[0x300] = { 0 }, hmac[0x20] = { 0 }; + 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; } @@ -342,14 +530,17 @@ int main(int argc, char **argv) textBuf = C2D_TextBufNew(8192); measureBuf = C2D_TextBufNew(512); hidSetRepeatParameters(18, 4); + pics_init(); catalog_start(); - int tab = 0, selected = 0, scroll = 0; - bool ready = false; - char msg[192] = ""; - int armed = -1; // Sicherheitsabfrage: 0 = installieren, 1 = entfernen - bool quit = false; - int touchStartY = -1, touchScroll = 0, touchRow = -1; + 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()) { @@ -357,8 +548,8 @@ int main(int argc, char **argv) u32 down = hidKeysDown(), rep = hidKeysDownRepeat(), held = hidKeysHeld(); bool busy = job_state() == JOB_RUNNING; if (down & KEY_START && !busy) break; + pics_poll(); - // Katalog fertig: Symbole laden und Liste aufbauen if (!ready && catalog_state() == CAT_READY) { load_sheets(); rebuild_view(tab); @@ -366,104 +557,163 @@ int main(int argc, char **argv) } if (catalog_state() == CAT_ERROR && down & KEY_A) catalog_start(); - // Ergebnis einer Installation: anzeigen, Liste auffrischen (z. B. Reiter "Meine") + // Ergebnis einer Installation JobState js = job_state(); if (js == JOB_DONE || js == JOB_FAILED) { - snprintf(msg, sizeof(msg), "%s", job_message()); + 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; } - if (ready && !busy) { + 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 = scroll = 0; } - if (rep & (KEY_DOWN | KEY_CPAD_DOWN)) selected++; - if (rep & (KEY_UP | KEY_CPAD_UP)) selected--; - if (rep & (KEY_RIGHT | KEY_CPAD_RIGHT)) selected += VISIBLE_ROWS - 1; - if (rep & (KEY_LEFT | KEY_CPAD_LEFT)) selected -= VISIBLE_ROWS - 1; + 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, Liste ziehen, Zeile antippen (zweimal = Aktion) - touchPosition tp; + // 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) tab = tp.px * TAB_COUNT / BOT_W; - else { touchStartY = tp.py; touchScroll = scroll; dragged = false; touchRow = scroll + (tp.py - LIST_Y) / ROW_H; } + 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) > 6) dragged = true; + if (abs(dy) > 8) dragged = true; if (dragged) { - scroll = touchScroll + dy / ROW_H; - int maxScroll = viewCount - (VISIBLE_ROWS - 1); - if (scroll > maxScroll) scroll = maxScroll; - if (scroll < 0) scroll = 0; + 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 && touchRow >= 0 && touchRow < viewCount) { - if (touchRow == selected) down |= KEY_A; - else selected = touchRow; - } + if (!dragged && touchCell >= 0 && touchCell < viewCount) { selected = touchCell; down |= KEY_A; } touchStartY = -1; } - if (tab != oldTab) { rebuild_view(tab); selected = scroll = 0; } + // 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 (selected != oldSel || tab != oldTab) { msg[0] = 0; armed = -1; } - if (selected < scroll) scroll = selected; - if (selected >= scroll + VISIBLE_ROWS - 1) scroll = selected - (VISIBLE_ROWS - 2); - - App *a = viewCount ? catalog_app(view[selected]) : NULL; + 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) { - AppStatus s = app_status(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 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 (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_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, msg, sizeof(msg))) quit = true; + if (launch(a, toast, sizeof(toast))) quit = true; + else { toastColor = COL_AMBER; toastFrames = 240; } } else if (armed == 0) { armed = -1; - char size[16]; - size_text(size, sizeof(size), a->size); - snprintf(msg, sizeof(msg), "Lade %s (%s) ...", a->title, size); job_install(a); } else if (catalog_offline()) { - snprintf(msg, sizeof(msg), "Keine Verbindung zum MBMNET-Server.\nInstallieren geht gerade nicht."); - } else { - armed = 0; - snprintf(msg, sizeof(msg), "%s %s?\nZum Bestätigen nochmal A drücken (oder nochmal antippen).", - a->title, s == APP_UPDATE ? "aktualisieren" : "installieren"); - } + 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 (a && down & KEY_Y && app_status(a) != APP_NEW) { - if (armed == 1) { - armed = -1; - snprintf(msg, sizeof(msg), "Entferne %s ...", a->title); - job_remove(a); - } else { - armed = 1; - snprintf(msg, sizeof(msg), "%s entfernen?\nZum Bestätigen nochmal Y drücken.", a->title); - } - } - if (down & KEY_B) { msg[0] = 0; 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); - draw_top(cur, tab, msg, armed >= 0); + 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); - draw_bottom(tab, selected, scroll); + 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); diff --git a/shop/source/pics.c b/shop/source/pics.c new file mode 100644 index 0000000..0b3b866 --- /dev/null +++ b/shop/source/pics.c @@ -0,0 +1,170 @@ +// Vorschaubilder im Hintergrund laden (immer nur das zuletzt gewünschte, ältere Wünsche verfallen). +#include <3ds.h> +#include +#include +#include + +#include "net.h" +#include "pics.h" +#include "store.h" + +#define SLOTS 8 +#define TEX_BYTES (512 * 256 * 2) + +typedef struct { + char key[96]; + PicState state; + C3D_Tex tex; + Tex3DS_SubTexture sub; + bool hasTex; + u32 used; +} Pic; + +static Pic slots[SLOTS]; +static u32 tick; + +// Austausch mit dem Lade-Thread +static LightLock lock; +static LightEvent wake; +static char wantKey[96], wantUrl[192]; +static char busyKey[96]; // lädt gerade +static char doneKey[96]; +static u8 *doneBuf; +static bool doneOk, hasDone, quit; +static Thread thread; + +static void worker(void *arg) +{ + (void)arg; + char key[96], url[192]; + while (true) { + LightEvent_Wait(&wake); + LightLock_Lock(&lock); + if (quit) { LightLock_Unlock(&lock); return; } + snprintf(key, sizeof(key), "%s", wantKey); + snprintf(url, sizeof(url), "%s", wantUrl); + wantKey[0] = 0; + snprintf(busyKey, sizeof(busyKey), "%s", key); + LightLock_Unlock(&lock); + if (!key[0]) continue; + + u8 *data = NULL; + u32 size = 0; + Result rc; + u32 status = net_get(url, &data, &size, TEX_BYTES, &rc); + bool ok = status == 200 && size == TEX_BYTES; + if (!ok) { free(data); data = NULL; } + + LightLock_Lock(&lock); + free(doneBuf); // nicht abgeholt (sollte nicht vorkommen) + snprintf(doneKey, sizeof(doneKey), "%s", key); + busyKey[0] = 0; + doneBuf = data; + // Netzwerkfehler (status 0): nicht als "kein Bild" merken, später nochmal versuchen + doneOk = ok || status != 0; + hasDone = true; + LightLock_Unlock(&lock); + } +} + +void pics_init(void) +{ + LightLock_Init(&lock); + LightEvent_Init(&wake, RESET_ONESHOT); + s32 prio = 0x30; + svcGetThreadPriority(&prio, CUR_THREAD_HANDLE); + thread = threadCreate(worker, NULL, 16 * 1024, prio + 1, -2, false); +} + +void pics_exit(void) +{ + if (thread) { + LightLock_Lock(&lock); + quit = true; + LightLock_Unlock(&lock); + LightEvent_Signal(&wake); + threadJoin(thread, U64_MAX); + threadFree(thread); + } + for (int i = 0; i < SLOTS; i++) if (slots[i].hasTex) C3D_TexDelete(&slots[i].tex); + free(doneBuf); +} + +static Pic *find(const char *key) +{ + for (int i = 0; i < SLOTS; i++) if (slots[i].key[0] && strcmp(slots[i].key, key) == 0) return &slots[i]; + return NULL; +} + +// Platz für ein neues Bild: frei oder am längsten nicht benutzt (nie das, was gerade lädt) +static Pic *take_slot(void) +{ + Pic *best = NULL; + LightLock_Lock(&lock); + for (int i = 0; i < SLOTS; i++) { + Pic *p = &slots[i]; + if (!p->key[0]) { best = p; break; } + if (!strcmp(p->key, busyKey) || !strcmp(p->key, wantKey) || (hasDone && !strcmp(p->key, doneKey))) continue; + if (!best || p->used < best->used) best = p; + } + LightLock_Unlock(&lock); + if (best && best->hasTex) { C3D_TexDelete(&best->tex); best->hasTex = false; } + if (best) best->key[0] = 0; + return best; +} + +void pics_poll(void) +{ + tick++; + LightLock_Lock(&lock); + if (!hasDone) { LightLock_Unlock(&lock); return; } + char key[96]; + snprintf(key, sizeof(key), "%s", doneKey); + u8 *buf = doneBuf; + bool ok = doneOk; + doneBuf = NULL; + hasDone = false; + LightLock_Unlock(&lock); + + Pic *p = find(key); + if (p && buf && C3D_TexInit(&p->tex, 512, 256, GPU_RGB565)) { + memcpy(p->tex.data, buf, TEX_BYTES); + C3D_TexFlush(&p->tex); + C3D_TexSetFilter(&p->tex, GPU_LINEAR, GPU_LINEAR); + p->sub = (Tex3DS_SubTexture){ 400, 240, 0.0f, 1.0f, 400.0f / 512.0f, 1.0f - 240.0f / 256.0f }; + p->hasTex = true; + p->state = PIC_READY; + } else if (p) { + if (ok) p->state = PIC_MISSING; + else p->key[0] = 0; // Netzwerkfehler: beim nächsten Mal neu versuchen + } + free(buf); +} + +PicState pics_get(const char *slug, int which, C2D_Image *out) +{ + char key[96]; + snprintf(key, sizeof(key), "%s/%d", slug, which); + Pic *p = find(key); + if (p) { + p->used = tick; + if (p->state == PIC_READY) { out->tex = &p->tex; out->subtex = &p->sub; } + // Lädt angeblich, aber der Thread hat inzwischen ein anderes Bild gewollt: nochmal anfordern + if (p->state != PIC_LOADING) return p->state; + } else { + p = take_slot(); + if (!p) return PIC_LOADING; + snprintf(p->key, sizeof(p->key), "%s", key); + p->state = PIC_LOADING; + p->used = tick; + } + LightLock_Lock(&lock); + if (strcmp(wantKey, key) != 0 && strcmp(busyKey, key) != 0) { + snprintf(wantKey, sizeof(wantKey), "%s", key); + if (which == PIC_BANNER) snprintf(wantUrl, sizeof(wantUrl), "http://%s/shop/banner/%s", SHOP_SERVER, slug); + else snprintf(wantUrl, sizeof(wantUrl), "http://%s/shop/shot/%s/%d", SHOP_SERVER, slug, which); + LightEvent_Signal(&wake); + } + LightLock_Unlock(&lock); + return PIC_LOADING; +} diff --git a/shop/source/pics.h b/shop/source/pics.h new file mode 100644 index 0000000..ea55908 --- /dev/null +++ b/shop/source/pics.h @@ -0,0 +1,16 @@ +#pragma once +#include +#include + +// Vorschaubilder (Screenshots, Banner) für die Shop-Seiten. Der Server liefert sie als fertige +// 3DS-Textur (512x256 RGB565, sichtbar 400x240), geladen wird im Hintergrund. +#define PIC_BANNER -1 + +typedef enum { PIC_LOADING, PIC_READY, PIC_MISSING } PicState; + +void pics_init(void); +void pics_exit(void); +// Jedes Bild einmal pro Bild aufrufen: holt fertige Downloads in den Grafikspeicher +void pics_poll(void); +// Bild für eine App (which = Screenshot-Nummer oder PIC_BANNER). Startet bei Bedarf den Download. +PicState pics_get(const char *slug, int which, C2D_Image *out); diff --git a/shop/source/store.c b/shop/source/store.c index ebb64b3..965bbd4 100644 --- a/shop/source/store.c +++ b/shop/source/store.c @@ -96,7 +96,9 @@ static bool parse_list(char *text, int *revision) { char *save = NULL, *line = strtok_r(text, "\n", &save); char *f[16]; - if (!line || split(line, f, 16) < 5 || strcmp(f[0], "MBMNETSHOP") != 0) return false; + if (!line || split(line, f, 20) < 5 || strcmp(f[0], "MBMNETSHOP") != 0) return false; + int format = atoi(f[1]); + int fields = format >= 2 ? 17 : 13; *revision = atoi(f[2]); int count = atoi(f[3]); sheetCount = atoi(f[4]); @@ -107,7 +109,7 @@ static bool parse_list(char *text, int *revision) appCount = 0; while ((line = strtok_r(NULL, "\n", &save)) && appCount < count) { - if (split(line, f, 16) < 13) continue; + if (split(line, f, 20) < fields) continue; App *a = &apps[appCount++]; COPY(a->slug, f[0]); COPY(a->title, f[1]); @@ -121,7 +123,13 @@ static bool parse_list(char *text, int *revision) a->size = strtoul(f[9], NULL, 10); a->stars = strtoul(f[10], NULL, 10); COPY(a->updated, f[11]); - a->description = strdup(f[12]); + if (format >= 2) { + a->shots = atoi(f[12]); + a->banner = f[13][0] == '1'; + a->colorBg = f[14][0] == '#' ? strtoul(f[14] + 1, NULL, 16) : 0; + COPY(a->license, f[15]); + } + a->description = strdup(f[fields - 1]); } return appCount > 0; } @@ -207,7 +215,7 @@ static void catalog_worker(void *arg) mkdir(SHOP_DIR, 0777); snprintf(catMsg, sizeof(catMsg), "Lade den Katalog ..."); - snprintf(url, sizeof(url), "http://%s/shop/list", SHOP_SERVER); + snprintf(url, sizeof(url), "http://%s/shop/list2", SHOP_SERVER); u32 status = net_get(url, &data, &size, 4 * 1024 * 1024, &rc); char *text = NULL; if (status == 200 && size > 10) { diff --git a/shop/source/store.h b/shop/source/store.h index 0ceac7d..e730d07 100644 --- a/shop/source/store.h +++ b/shop/source/store.h @@ -12,6 +12,10 @@ typedef struct { char slug[72], title[64], author[44], version[28], category[16], kind[6], file[72], updated[12]; int sheet, icon; // Symbol: Blatt und Nummer darin (-1 = keins) u32 size, stars; + int shots; // Anzahl Screenshots (/shop/shot//) + bool banner; // Bannerbild vorhanden (/shop/banner/) + u32 colorBg; // Hintergrundfarbe der App (0xRRGGBB), 0 = keine + char license[44]; char *description; // Installiert (aus SHOP_DIR/installed.txt) bool installed;