v2.9.1: Shop im eShop-Stil mit Screenshots und Suche, GodMode9-Skript zum Installieren
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ca77952a9d
commit
1f69ff6c56
@@ -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))
|
||||
|
||||
@@ -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
|
||||
+3
-3
@@ -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
|
||||
|
||||
+436
-186
@@ -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 <citro2d.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#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);
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
// Vorschaubilder im Hintergrund laden (immer nur das zuletzt gewünschte, ältere Wünsche verfallen).
|
||||
#include <3ds.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include <citro2d.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// 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);
|
||||
+12
-4
@@ -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) {
|
||||
|
||||
@@ -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/<slug>/<n>)
|
||||
bool banner; // Bannerbild vorhanden (/shop/banner/<slug>)
|
||||
u32 colorBg; // Hintergrundfarbe der App (0xRRGGBB), 0 = keine
|
||||
char license[44];
|
||||
char *description;
|
||||
// Installiert (aus SHOP_DIR/installed.txt)
|
||||
bool installed;
|
||||
|
||||
Reference in New Issue
Block a user