467 lines
11 KiB
C
467 lines
11 KiB
C
#include <3ds.h>
|
|
#include <citro2d.h>
|
|
#include <citro3d.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <png.h>
|
|
#include "gui.h"
|
|
#include "metadata.h"
|
|
|
|
static C3D_RenderTarget* topTarget = NULL;
|
|
static C3D_RenderTarget* bottomTarget = NULL;
|
|
static C2D_TextBuf textBuf = NULL;
|
|
|
|
/**
|
|
* Initialize the GUI system
|
|
*/
|
|
int guiInit(void)
|
|
{
|
|
gfxInitDefault();
|
|
C3D_Init(C3D_DEFAULT_CMDBUF_SIZE);
|
|
C2D_Init(C2D_DEFAULT_MAX_OBJECTS);
|
|
C2D_Prepare();
|
|
|
|
/* Create render targets for top and bottom screens */
|
|
topTarget = C2D_CreateScreenTarget(GFX_TOP, GFX_LEFT);
|
|
bottomTarget = C2D_CreateScreenTarget(GFX_BOTTOM, GFX_LEFT);
|
|
|
|
if(!topTarget || !bottomTarget)
|
|
return -1;
|
|
|
|
/* Create text buffer */
|
|
textBuf = C2D_TextBufNew(4096);
|
|
if(!textBuf)
|
|
return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Clean up and exit the GUI system
|
|
*/
|
|
void guiExit(void)
|
|
{
|
|
if(textBuf)
|
|
C2D_TextBufDelete(textBuf);
|
|
|
|
C2D_Fini();
|
|
C3D_Fini();
|
|
gfxExit();
|
|
}
|
|
|
|
/**
|
|
* Begin rendering a frame
|
|
*/
|
|
void guiBeginFrame(void)
|
|
{
|
|
C3D_FrameBegin(C3D_FRAME_SYNCDRAW);
|
|
}
|
|
|
|
/**
|
|
* End rendering a frame and display it
|
|
*/
|
|
void guiEndFrame(void)
|
|
{
|
|
C3D_FrameEnd(0);
|
|
}
|
|
|
|
/**
|
|
* Clear the top screen
|
|
*/
|
|
void guiClearTopScreen(void)
|
|
{
|
|
C2D_TargetClear(topTarget, GUI_COLOR_BG_TOP);
|
|
}
|
|
|
|
/**
|
|
* Clear the bottom screen
|
|
*/
|
|
void guiClearBottomScreen(void)
|
|
{
|
|
C2D_TargetClear(bottomTarget, GUI_COLOR_BG_BOTTOM);
|
|
}
|
|
|
|
/**
|
|
* Draw a simple text string at specified position
|
|
*/
|
|
void guiDrawText(gfxScreen_t screen, float x, float y, const char* text, u32 color, float scale)
|
|
{
|
|
if(!text || !textBuf)
|
|
return;
|
|
|
|
C2D_Text c2dText;
|
|
C2D_TextBufClear(textBuf);
|
|
C2D_TextParse(&c2dText, textBuf, text);
|
|
C2D_TextOptimize(&c2dText);
|
|
|
|
C3D_RenderTarget* target = (screen == GFX_TOP) ? topTarget : bottomTarget;
|
|
C2D_SceneBegin(target);
|
|
C2D_DrawText(&c2dText, C2D_WithColor, x, y, 0.5f, scale, scale, color);
|
|
}
|
|
|
|
/**
|
|
* Display metadata on the top screen
|
|
*/
|
|
void guiDisplayMetadata(struct metadata_t* metadata, const char* filename)
|
|
{
|
|
if(!metadata || !filename || !textBuf)
|
|
return;
|
|
|
|
C2D_SceneBegin(topTarget);
|
|
|
|
/* Extract just the filename without path and extension for fallback */
|
|
const char* basename = strrchr(filename, '/');
|
|
if(!basename)
|
|
basename = filename;
|
|
else
|
|
basename++;
|
|
|
|
/* Remove file extension for display */
|
|
char displayName[64];
|
|
strncpy(displayName, basename, sizeof(displayName) - 1);
|
|
displayName[sizeof(displayName) - 1] = '\0';
|
|
char* dot = strrchr(displayName, '.');
|
|
if(dot) *dot = '\0';
|
|
|
|
C2D_Text text;
|
|
float y = 10.0f;
|
|
float scale = 0.6f;
|
|
float lineHeight = 20.0f;
|
|
|
|
C2D_TextBufClear(textBuf);
|
|
|
|
/* Draw title */
|
|
if(metadata->title[0])
|
|
{
|
|
char titleBuf[64];
|
|
snprintf(titleBuf, sizeof(titleBuf), "%.47s", metadata->title);
|
|
C2D_TextParse(&text, textBuf, titleBuf);
|
|
C2D_TextOptimize(&text);
|
|
C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, scale, scale, GUI_COLOR_TEXT);
|
|
}
|
|
else
|
|
{
|
|
char titleBuf[64];
|
|
snprintf(titleBuf, sizeof(titleBuf), "%.47s", displayName);
|
|
C2D_TextParse(&text, textBuf, titleBuf);
|
|
C2D_TextOptimize(&text);
|
|
C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, scale, scale, GUI_COLOR_TEXT);
|
|
}
|
|
y += lineHeight;
|
|
|
|
/* Draw artist */
|
|
if(metadata->artist[0])
|
|
{
|
|
char artistBuf[64];
|
|
snprintf(artistBuf, sizeof(artistBuf), "%.45s", metadata->artist);
|
|
C2D_TextParse(&text, textBuf, artistBuf);
|
|
C2D_TextOptimize(&text);
|
|
C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, scale * 0.8f, scale * 0.8f, GUI_COLOR_TEXT_DIM);
|
|
}
|
|
else
|
|
{
|
|
C2D_TextParse(&text, textBuf, "Unknown Artist");
|
|
C2D_TextOptimize(&text);
|
|
C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, scale * 0.8f, scale * 0.8f, GUI_COLOR_TEXT_DIM);
|
|
}
|
|
y += lineHeight;
|
|
|
|
/* Draw album */
|
|
if(metadata->album[0])
|
|
{
|
|
char albumBuf[64];
|
|
snprintf(albumBuf, sizeof(albumBuf), "%.45s", metadata->album);
|
|
C2D_TextParse(&text, textBuf, albumBuf);
|
|
C2D_TextOptimize(&text);
|
|
C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, scale * 0.8f, scale * 0.8f, GUI_COLOR_TEXT_DIM);
|
|
}
|
|
else
|
|
{
|
|
C2D_TextParse(&text, textBuf, "Unknown Album");
|
|
C2D_TextOptimize(&text);
|
|
C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, scale * 0.8f, scale * 0.8f, GUI_COLOR_TEXT_DIM);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display log messages on the top screen
|
|
*/
|
|
void guiDisplayLog(const char** messages, int count, int scroll)
|
|
{
|
|
if(!messages || count <= 0 || !textBuf)
|
|
return;
|
|
|
|
C2D_SceneBegin(topTarget);
|
|
|
|
C2D_Text text;
|
|
float y = 70.0f; /* Start below metadata area */
|
|
float scale = 0.4f;
|
|
float lineHeight = 12.0f;
|
|
int maxLines = 14;
|
|
|
|
C2D_TextBufClear(textBuf);
|
|
|
|
for(int i = scroll; i < count && (i - scroll) < maxLines; i++)
|
|
{
|
|
if(messages[i])
|
|
{
|
|
C2D_TextParse(&text, textBuf, messages[i]);
|
|
C2D_TextOptimize(&text);
|
|
C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, scale, scale, GUI_COLOR_TEXT);
|
|
y += lineHeight;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display file list on the bottom screen
|
|
*/
|
|
void guiDisplayFileList(const char** files, int count, int selected, int scroll)
|
|
{
|
|
if(!files || count <= 0 || !textBuf)
|
|
return;
|
|
|
|
C2D_SceneBegin(bottomTarget);
|
|
|
|
C2D_Text text;
|
|
float y = 10.0f;
|
|
float scale = 0.5f;
|
|
float lineHeight = 16.0f;
|
|
int maxLines = 14;
|
|
|
|
C2D_TextBufClear(textBuf);
|
|
|
|
for(int i = scroll; i < count && (i - scroll) < maxLines; i++)
|
|
{
|
|
if(files[i])
|
|
{
|
|
/* Check if this is a directory */
|
|
size_t len = strlen(files[i]);
|
|
bool isDir = (len > 0 && files[i][len-1] == '/');
|
|
|
|
/* Draw selection highlight */
|
|
if(i == selected)
|
|
{
|
|
C2D_DrawRectSolid(5.0f, y - 2.0f, 0.5f, 310.0f, lineHeight, GUI_COLOR_HIGHLIGHT);
|
|
}
|
|
|
|
/* Truncate if too long */
|
|
char displayName[48];
|
|
snprintf(displayName, sizeof(displayName), "%.40s", files[i]);
|
|
|
|
C2D_TextParse(&text, textBuf, displayName);
|
|
C2D_TextOptimize(&text);
|
|
|
|
/* Use different color for directories */
|
|
u32 color;
|
|
if(i == selected)
|
|
color = GUI_COLOR_ACCENT;
|
|
else if(isDir)
|
|
color = C2D_Color32(100, 200, 255, 255); /* Light blue for folders */
|
|
else
|
|
color = GUI_COLOR_TEXT;
|
|
|
|
C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, scale, scale, color);
|
|
y += lineHeight;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display playback controls and status on the top screen
|
|
*/
|
|
void guiDisplayPlaybackStatus(bool isPlaying, bool isPaused, float position, float duration)
|
|
{
|
|
if(!textBuf)
|
|
return;
|
|
|
|
C2D_SceneBegin(topTarget);
|
|
|
|
C2D_Text text;
|
|
C2D_TextBufClear(textBuf);
|
|
|
|
/* Display status and time at bottom of top screen */
|
|
float y = 215.0f;
|
|
|
|
/* Display status */
|
|
char statusBuf[64];
|
|
if(isPlaying)
|
|
{
|
|
if(isPaused)
|
|
snprintf(statusBuf, sizeof(statusBuf), "Paused");
|
|
else
|
|
snprintf(statusBuf, sizeof(statusBuf), "Playing");
|
|
}
|
|
else
|
|
{
|
|
snprintf(statusBuf, sizeof(statusBuf), "Stopped");
|
|
}
|
|
|
|
C2D_TextParse(&text, textBuf, statusBuf);
|
|
C2D_TextOptimize(&text);
|
|
C2D_DrawText(&text, C2D_WithColor, 10.0f, y, 0.5f, 0.5f, 0.5f, GUI_COLOR_TEXT);
|
|
|
|
/* Display time if playing */
|
|
if(isPlaying && duration > 0)
|
|
{
|
|
char timeBuf[32];
|
|
int posMin = (int)position / 60;
|
|
int posSec = (int)position % 60;
|
|
int durMin = (int)duration / 60;
|
|
int durSec = (int)duration % 60;
|
|
snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d / %02d:%02d", posMin, posSec, durMin, durSec);
|
|
|
|
C2D_TextParse(&text, textBuf, timeBuf);
|
|
C2D_TextOptimize(&text);
|
|
C2D_DrawText(&text, C2D_WithColor, 280.0f, y, 0.5f, 0.5f, 0.5f, GUI_COLOR_TEXT);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display version text and credits at bottom of bottom screen
|
|
*/
|
|
void guiDisplayVersion(const char* version)
|
|
{
|
|
if(!textBuf)
|
|
return;
|
|
|
|
C2D_SceneBegin(bottomTarget);
|
|
|
|
C2D_Text text;
|
|
C2D_TextBufClear(textBuf);
|
|
|
|
/* Display "mice - by sillyangel" at bottom center */
|
|
const char* credits = "mice - by sillyangel";
|
|
C2D_TextParse(&text, textBuf, credits);
|
|
C2D_TextOptimize(&text);
|
|
C2D_DrawText(&text, C2D_WithColor, 80.0f, 220.0f, 0.5f, 0.45f, 0.45f, GUI_COLOR_TEXT_DIM);
|
|
}
|
|
|
|
/**
|
|
* PNG read callback for memory buffer
|
|
*/
|
|
static void pngReadCallback(png_structp png_ptr, png_bytep data, png_size_t length)
|
|
{
|
|
uint8_t** buffer_ptr = (uint8_t**)png_get_io_ptr(png_ptr);
|
|
memcpy(data, *buffer_ptr, length);
|
|
*buffer_ptr += length;
|
|
}
|
|
|
|
/**
|
|
* Display album art on top screen
|
|
*/
|
|
void guiDisplayAlbumArt(struct metadata_t* metadata)
|
|
{
|
|
if(!metadata || !metadata->hasAlbumArt || !metadata->albumArt)
|
|
return;
|
|
|
|
C2D_SceneBegin(topTarget);
|
|
|
|
/* Decode PNG image */
|
|
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
|
|
if(!png_ptr)
|
|
return;
|
|
|
|
png_infop info_ptr = png_create_info_struct(png_ptr);
|
|
if(!info_ptr)
|
|
{
|
|
png_destroy_read_struct(&png_ptr, NULL, NULL);
|
|
return;
|
|
}
|
|
|
|
if(setjmp(png_jmpbuf(png_ptr)))
|
|
{
|
|
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
|
|
return;
|
|
}
|
|
|
|
/* Set up custom read function */
|
|
uint8_t* buffer_ptr = metadata->albumArt;
|
|
png_set_read_fn(png_ptr, &buffer_ptr, pngReadCallback);
|
|
|
|
/* Read PNG info */
|
|
png_read_info(png_ptr, info_ptr);
|
|
|
|
int width = png_get_image_width(png_ptr, info_ptr);
|
|
int height = png_get_image_height(png_ptr, info_ptr);
|
|
png_byte color_type = png_get_color_type(png_ptr, info_ptr);
|
|
png_byte bit_depth = png_get_bit_depth(png_ptr, info_ptr);
|
|
|
|
/* Convert to RGBA8 */
|
|
if(bit_depth == 16)
|
|
png_set_strip_16(png_ptr);
|
|
if(color_type == PNG_COLOR_TYPE_PALETTE)
|
|
png_set_palette_to_rgb(png_ptr);
|
|
if(color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
|
|
png_set_expand_gray_1_2_4_to_8(png_ptr);
|
|
if(png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
|
|
png_set_tRNS_to_alpha(png_ptr);
|
|
if(color_type == PNG_COLOR_TYPE_RGB ||
|
|
color_type == PNG_COLOR_TYPE_GRAY ||
|
|
color_type == PNG_COLOR_TYPE_PALETTE)
|
|
png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER);
|
|
if(color_type == PNG_COLOR_TYPE_GRAY ||
|
|
color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
|
|
png_set_gray_to_rgb(png_ptr);
|
|
|
|
png_read_update_info(png_ptr, info_ptr);
|
|
|
|
/* Allocate image buffer */
|
|
png_bytep* row_pointers = (png_bytep*)malloc(sizeof(png_bytep) * height);
|
|
if(!row_pointers)
|
|
{
|
|
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
|
|
return;
|
|
}
|
|
|
|
for(int y = 0; y < height; y++)
|
|
{
|
|
row_pointers[y] = (png_byte*)malloc(png_get_rowbytes(png_ptr, info_ptr));
|
|
if(!row_pointers[y])
|
|
{
|
|
for(int i = 0; i < y; i++)
|
|
free(row_pointers[i]);
|
|
free(row_pointers);
|
|
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Read image data */
|
|
png_read_image(png_ptr, row_pointers);
|
|
|
|
/* Scale to fit in top-right corner (max 60x60) */
|
|
int displayWidth = width;
|
|
int displayHeight = height;
|
|
if(width > 60 || height > 60)
|
|
{
|
|
float scale = 60.0f / (width > height ? width : height);
|
|
displayWidth = (int)(width * scale);
|
|
displayHeight = (int)(height * scale);
|
|
}
|
|
|
|
/* Draw the image pixel by pixel */
|
|
float startX = 330.0f; /* Top right corner */
|
|
float startY = 10.0f;
|
|
|
|
for(int y = 0; y < displayHeight; y++)
|
|
{
|
|
int srcY = (y * height) / displayHeight;
|
|
for(int x = 0; x < displayWidth; x++)
|
|
{
|
|
int srcX = (x * width) / displayWidth;
|
|
png_bytep px = &(row_pointers[srcY][srcX * 4]);
|
|
u32 color = C2D_Color32(px[0], px[1], px[2], px[3]);
|
|
C2D_DrawRectSolid(startX + x, startY + y, 0.5f, 1, 1, color);
|
|
}
|
|
}
|
|
|
|
/* Clean up */
|
|
for(int y = 0; y < height; y++)
|
|
free(row_pointers[y]);
|
|
free(row_pointers);
|
|
|
|
png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
|
|
}
|