Inline base64 icons in gamelist HTML - instant on all devices, no separate HTTP requests

This commit is contained in:
KarnerF committed 2026-08-30 18:11:57 +02:00
1 parent 2f608f8aa8
commit 81d41c15a8
1 file changed
+60 -6
+60 -6
View File
@@ -241,6 +241,57 @@ static void get_sm_version(char *buf, size_t buflen) {
}
/* Generic SM API proxy - returns malloc'd body or NULL */
/* base64-encode binary data; out must be at least ceil(in_len/3)*4+1 bytes */
static void b64enc(const unsigned char *in, size_t len, char *out) {
static const char t[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
size_t i=0,j=0;
for(;i+2<len;i+=3){
out[j++]=t[in[i]>>2];out[j++]=t[((in[i]&3)<<4)|(in[i+1]>>4)];
out[j++]=t[((in[i+1]&15)<<2)|(in[i+2]>>6)];out[j++]=t[in[i+2]&63];
}
if(i<len){
out[j++]=t[in[i]>>2];
if(i+1<len){out[j++]=t[((in[i]&3)<<4)|(in[i+1]>>4)];out[j++]=t[(in[i+1]&15)<<2];out[j++]='=';}
else{out[j++]=t[(in[i]&3)<<4];out[j++]='=';out[j++]='=';}
}
out[j]=0;
}
/* read icon file and return malloc'd base64 string, or NULL */
static char *icon_b64(const char *tid) {
static const char *ptpl[]={
"/data/SMPlusGui/icon-cache/%s.png",
"/user/appmeta/%s/icon0.png",
"/data/homebrew/%s/sce_sys/icon0.png",
NULL
};
char src[256]; FILE *f=NULL;
for(int i=0;ptpl[i]&&!f;i++){snprintf(src,sizeof(src),ptpl[i],tid);f=fopen(src,"rb");}
/* scan /mnt/shadowmnt/ */
if(!f){DIR *sd=opendir("/mnt/shadowmnt");if(sd){struct dirent *de;size_t tl=strlen(tid);
while(!f&&(de=readdir(sd))!=NULL)if(strncmp(de->d_name,tid,tl)==0&&de->d_name[tl]=='_')
{snprintf(src,sizeof(src),"/mnt/shadowmnt/%s/sce_sys/icon0.png",de->d_name);f=fopen(src,"rb");}
closedir(sd);}
}
if(!f) return NULL;
fseek(f,0,SEEK_END);long sz=ftell(f);fseek(f,0,SEEK_SET);
if(sz<=0||sz>524288){fclose(f);return NULL;}
unsigned char *buf=malloc(sz);
if(!buf){fclose(f);return NULL;}
if((long)fread(buf,1,sz,f)!=sz){free(buf);fclose(f);return NULL;}
fclose(f);
/* also write to cache if we found it elsewhere */
{char cp[256];snprintf(cp,sizeof(cp),"/data/SMPlusGui/icon-cache/%s.png",tid);
struct stat cs; if(stat(cp,&cs)!=0){mkdir("/data/SMPlusGui",0777);mkdir("/data/SMPlusGui/icon-cache",0777);
FILE *cf=fopen(cp,"wb");if(cf){fwrite(buf,1,sz,cf);fclose(cf);}}}
size_t b64sz=((sz+2)/3)*4+1;
char *out=malloc(b64sz);
if(!out){free(buf);return NULL;}
b64enc(buf,sz,out);
free(buf);
return out;
}
static char* sm_api_req(const char *method, const char *path, const char *body) {
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) return NULL;
@@ -2901,9 +2952,9 @@ static void fn(struct mg_connection *c, int ev, void *ev_data) {
/* images always available; enrich with title_name from games API when mounted */
char *img_json=sm_api_req("POST","/api/v1/images","{}");
char *gam_json=sm_api_req("POST","/api/v1/games","{}");
char *out=malloc(131072); if(!out){if(img_json)free(img_json);if(gam_json)free(gam_json);mg_http_reply(c,500,"","");return;}
char *out=malloc(1048576); if(!out){if(img_json)free(img_json);if(gam_json)free(gam_json);mg_http_reply(c,500,"","");return;}
int pos=0;
#define GL(fmt,...) pos+=snprintf(out+pos,131071-pos,fmt,##__VA_ARGS__)
#define GL(fmt,...) pos+=snprintf(out+pos,1048575-pos,fmt,##__VA_ARGS__)
if(!img_json){
GL("<p class=\"hint\" style=\"color:#f87171;\">SM API nicht erreichbar</p>");
} else {
@@ -2912,7 +2963,7 @@ static void fn(struct mg_connection *c, int ev, void *ev_data) {
else {
int n=0;
const char *cur=istart;
while((cur=strstr(cur,"{\"path\""))!=NULL&&pos<120000){
while((cur=strstr(cur,"{\"path\""))!=NULL&&pos<900000){
char imgpath[512]={0},mp[256]={0};
long long sz=0; int mounted=0;
/* extract path, mount_point, size, mounted from images API */
@@ -2956,9 +3007,12 @@ static void fn(struct mg_connection *c, int ev, void *ev_data) {
const char *bs_um="background:transparent;border:1px solid #f87171;border-radius:4px;color:#f87171;padding:3px 10px;cursor:pointer;font-size:.75rem;";
const char *bs_mt="background:transparent;border:1px solid var(--accent);border-radius:4px;color:var(--accent);padding:3px 10px;cursor:pointer;font-size:.75rem;white-space:nowrap;";
GL("<div style=\"display:flex;align-items:center;gap:10px;padding:8px 4px;border-bottom:1px solid var(--border);\">");
GL("<div style=\"position:relative;width:48px;height:48px;border-radius:8px;overflow:hidden;background:#1e2a42;flex-shrink:0;\">"
"<img src=\"/api/icon/%s\" style=\"position:absolute;top:0;left:0;width:100%%;height:100%%;object-fit:cover;\" onerror=\"this.remove()\">"
"<span style=\"position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:.55rem;color:#64748b;font-family:monospace;\">%.4s</span></div>",tid,tid[0]?tid:"?");
/* inline base64 icon — no separate HTTP request, works on all devices */
{char *b64=tid[0]?icon_b64(tid):NULL;
if(b64){GL("<div style=\"width:48px;height:48px;border-radius:8px;overflow:hidden;flex-shrink:0;\">"
"<img src=\"data:image/png;base64,%s\" style=\"width:100%%;height:100%%;object-fit:cover;\"></div>",b64);free(b64);}
else GL("<div style=\"width:48px;height:48px;border-radius:8px;background:#1e2a42;flex-shrink:0;display:flex;align-items:center;"
"justify-content:center;font-size:.55rem;color:#64748b;font-family:monospace;\">%.4s</div>",tid[0]?tid:"?");}
GL("<div style=\"flex:1;min-width:0;\">"
"<div style=\"font-size:.83rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;\">%s</div>"
"<div style=\"font-family:var(--mono);font-size:.7rem;color:var(--dim);\">%s &mdash; %s</div></div>",