v2.9.0: MBMNET Shop (eigene App, auf Wunsch installierbar)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nico Meister
2026-10-18 17:46:42 +02:00
co-authored by Claude Opus 5.5
parent d264cd93cd
commit ca77952a9d
16 changed files with 1710 additions and 19 deletions
+477
View File
@@ -0,0 +1,477 @@
// 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.
#include <3ds.h>
#include <citro2d.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.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 TAB_H 26
#define ROW_H 40
#define LIST_Y (TAB_H + 2)
#define VISIBLE_ROWS ((SCR_H - LIST_Y) / ROW_H + 1)
// Kategorien (Universal-DB) und ihre Reiter
typedef struct { const char *label, *match; } Tab;
static const Tab TABS[] = {
{ "Alle", NULL }, { "Spiele", "game" }, { "Tools", "utility" }, { "Apps", "app" },
{ "Emus", "emulator" }, { "Medien", "media" }, { "Meine", "*installed" },
};
#define TAB_COUNT (int)(sizeof(TABS) / sizeof(TABS[0]))
static const char *SORT_NAMES[] = { "Name", "Beliebt", "Neu" };
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 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;
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)
{
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);
}
// Einkaufswagen (wie das Symbol der App)
static void draw_cart(float x, float y, float size, u32 bg)
{
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;
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);
(void)bg;
}
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, "?");
}
// --- Liste (gefiltert und sortiert) ---------------------------------------------
static int *view; // Indizes in den Katalog
static int viewCount;
static int sortMode;
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)) view[viewCount++] = i;
qsort(view, viewCount, sizeof(int), sortMode == 1 ? cmp_stars : sortMode == 2 ? cmp_new : cmp_name);
}
// --- Bildschirme ---------------------------------------------------------------
static void draw_status_badge(const App *a, float right, float y)
{
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);
}
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);
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);
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);
}
static void draw_bottom(int tab, int selected, int scroll)
{
C2D_DrawRectSolid(0, 0, 0, BOT_W, SCR_H, COL_BG);
// Reiter
float w = (float)BOT_W / TAB_COUNT;
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 (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);
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);
}
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);
}
}
// --- 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)) {
u8 param[0x300] = { 0 }, hmac[0x20] = { 0 };
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);
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;
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;
// Katalog fertig: Symbole laden und Liste aufbauen
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: anzeigen, Liste auffrischen (z. B. Reiter "Meine")
JobState js = job_state();
if (js == JOB_DONE || js == JOB_FAILED) {
snprintf(msg, sizeof(msg), "%s", job_message());
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 (ready && !busy) {
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;
// Touch: Reiter antippen, Liste ziehen, Zeile antippen (zweimal = Aktion)
touchPosition tp;
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; }
} else if (held & KEY_TOUCH && touchStartY >= 0) {
hidTouchRead(&tp);
int dy = touchStartY - tp.py;
if (abs(dy) > 6) 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;
}
} else if (touchStartY >= 0) {
if (!dragged && touchRow >= 0 && touchRow < viewCount) {
if (touchRow == selected) down |= KEY_A;
else selected = touchRow;
}
touchStartY = -1;
}
if (tab != oldTab) { rebuild_view(tab); selected = scroll = 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 (a && down & KEY_A) {
AppStatus s = app_status(a);
if (s == APP_INSTALLED && strcmp(a->kind, "cia") == 0) {
armed = -1;
if (launch(a, msg, sizeof(msg))) quit = true;
} 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");
}
}
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; }
}
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);
C2D_TargetClear(bot, COL_BG);
C2D_SceneBegin(bot);
draw_bottom(tab, selected, scroll);
C3D_FrameEnd(0);
}
store_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;
}