commit efe6cc1b2936b809a45bb017156868cdad37eb53 Author: Cavv Date: Thu Mar 13 03:12:51 2025 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9e299a --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build/ +Kekatsu.elf +Kekatsu.nds +release/ +Kekatsu-DS.zip +version.txt +.vscode/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a75b56c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Cavv + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d251433 --- /dev/null +++ b/Makefile @@ -0,0 +1,93 @@ +.SUFFIXES: + +ifeq ($(strip $(DEVKITARM)),) +$(error "Please set DEVKITARM in your environment. export DEVKITARM=devkitARM") +endif + +include $(DEVKITARM)/ds_rules + +TARGET := Kekatsu +BUILD := build +RELEASE := release +SOURCES := source source/utils source/gui +DATA := data +INCLUDES := include +GRAPHICS := source/gfx source/gui/gfx +LANGUAGES := source/lang +VERSION := 1.0.0 + +ARCH := -march=armv5te -mtune=arm946e-s -mthumb +CFLAGS := -g -Wall -O2 -ffunction-sections -fdata-sections $(ARCH) $(INCLUDE) -DARM9 +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions +ASFLAGS := -g $(ARCH) +LDFLAGS := -specs=ds_arm9.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) +LIBS := -lminizip -lcurl -lmbedtls -lmbedcrypto -lmbedx509 -lpng -lm -lz -ldswifi9 -lfat -lnds9 +LIBDIRS := $(PORTLIBS) $(LIBNDS) + +ifneq ($(BUILDDIR), $(CURDIR)) + +export OUTPUT := $(CURDIR)/$(TARGET) +export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ + $(foreach dir,$(DATA),$(CURDIR)/$(dir)) \ + $(foreach dir,$(GRAPHICS),$(CURDIR)/$(dir)) \ + $(foreach dir,$(LANGUAGES),$(CURDIR)/$(dir)) +export DEPSDIR := $(CURDIR)/$(BUILD) + +CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) +CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) +SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) +PNGFILES := $(foreach dir,$(GRAPHICS),$(notdir $(wildcard $(dir)/*.png))) +LANGFILES := $(foreach dir,$(LANGUAGES),$(notdir $(wildcard $(dir)/*.lang*))) + +ifeq ($(strip $(CPPFILES)),) +export LD := $(CC) +else +export LD := $(CXX) +endif + +export OFILES_LANG := $(addsuffix .o,$(LANGFILES)) +export OFILES_SOURCES := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) +export OFILES := $(PNGFILES:.png=.o) $(OFILES_LANG) $(OFILES_SOURCES) +export HFILES := $(PNGFILES:.png=.h) $(addsuffix .h,$(subst .,_,$(LANGFILES))) +export INCLUDE := $(foreach dir,$(INCLUDES),-iquote $(CURDIR)/$(dir)) \ + $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ + -I$(CURDIR)/$(BUILD) +export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) + +export GAME_TITLE := Kekatsu +export GAME_SUBTITLE1 := DS(i) content downloader +export GAME_SUBTITLE2 := Cavv +export GAME_ICON := $(CURDIR)/icon.bmp + +.PHONY: $(BUILD) clean + +$(BUILD): + @[ -d $@ ] || mkdir -p $@ + @make BUILDDIR=`cd $(BUILD) && pwd` --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile + +$(RELEASE): $(BUILD) + @[ -d $@ ] || mkdir -p $@ + @mv $(TARGET).nds $(RELEASE)/$(TARGET).nds + @echo $(VERSION) > $(RELEASE)/version.txt + +clean: + @echo clean ... + @rm -fr $(BUILD) $(TARGET).elf $(TARGET).nds $(RELEASE) + +else + +DEPENDS := $(OFILES:.o=.d) + +$(OUTPUT).nds: $(OUTPUT).elf +$(OUTPUT).elf: $(OFILES) + +%.s %.h: %.png %.grit + grit $< -fts -o$* + +%.lang.o %_lang.h : %.lang + @echo $(notdir $<) + @$(bin2o) + +-include $(DEPENDS) + +endif diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f9a2c8 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# Kekatsu DS +Easy-to-use content downloader for Nintendo DS(i) consoles + +![Screenshot 1](https://github.com/cavv-dev/Kekatsu-DS/raw/main/resources/screenshots/Kekatsu-DS_1.png) ![Screenshot 2](https://github.com/cavv-dev/Kekatsu-DS/raw/main/resources/screenshots/Kekatsu-DS_2.png) ![Screenshot 3](https://github.com/cavv-dev/Kekatsu-DS/raw/main/resources/screenshots/Kekatsu-DS_3.png) + +The main scope of this project is to have a standalone and easy way to download apps and games on the fly, given a database by the user. + +Concept inspired by [pkgi-psp](https://github.com/bucanero/pkgi-psp) and [Universal-Updater](https://github.com/Universal-Team/Universal-Updater). + +## Features +- Multi-platform content download +- Database loading via URL or file +- Support for multiple databases +- ZIP file extraction +- Automatic updates check +- Customizable color scheme +- Localization support + +## Databases setup +Kekatsu expects a `databases.txt` file to load as the list of available databases. This file has to be located in the `Kekatsu` directory on the root of the SD card. + +Each line of the `databases.txt` file has to contain a name and a value separated by a tab character. + +The name should be the display name of the database. The value must be either: +- an HTTP(S) URL which returns a database in text response +- a path to a database file on the SD card itself + +### Example databases.txt +``` +test-db https://example.com/database.txt +test-db2 /databases/db2.txt +``` + +## Community databases +Collection of useful known databases. Contact me to if you want to contribute to this list. + +| Name | Description | URL | +|---|---|---| +| [UDB-Kekatsu-DS](https://github.com/cavv-dev/UDB-Kekatsu-DS) | Updated selection of DS and DSi apps from Universal-DB | `https://gist.githubusercontent.com/cavv-dev/3c0cbc1b63ac8ca0c1d9f549403afbf1/raw/` | + +## Database creation instructions +A database file expected by Kekatsu is a text file that follows this precise structure: + +- **Line 1**: Database version - The database version to be used by the parser. Follow the next instructions that match the chosen database version. + +
Version 1 + +- **Line 2**: Delimiter character - The character to be used to separate fields in the next lines +- **Line 3 and above**: Fields separated by the delimiter character. They must follow this order: + - **Title** - Display title of the content + - **Platform** - Target platform of the content. *Should* be in lowercase and in its abbreviated form as it will be used as the name of the platform directory. E.g. `nds` instead of `Nintendo DS`. + - **Region** - Target region of the content. Could be `NTSC-U`, `PAL` and similar for contents which target a specific region or `ANY` for contents made for any region. + - **Version** - Release version of the content + - **Author** - Author or publisher of the content + - **Download URL** - The HTTP(S) URL to download the content. Must be a direct link to the file of the content. This file can be an executable or an archive in ZIP format. + - **File name** - The name under which the downloaded file will be saved + - **Size** - The size in bytes of the downloaded file + - **Box art URL** - The HTTP(S) URL of the displayed box art for the content. A box art is expected to be in PNG format. + - **Extract items** - The items to be extracted from the downloaded archive in couples of fields separated by the delimiter character. Each couple is composed of: + - **In-path** - The path of the file or directory in the archive to be extracted. Directories should have `/` as the last character. + - **Out-path** - The destination path of the extracted file or directory + + If no extract items are specified, all the files and directories will be extracted following the structure in the archive. + + They are not going to be checked if the downloaded file is not an archive. + +### Example database file +``` +1 +, +test-app,nds,ANY,1.0,Author1,https://example.com/test-app-v1.0.nds,test-app.nds,1048576,https://example.com/test-app-boxart.png +test-app2,gba,NTSC-U,1.1,Author2,https://example.com/test-app2.zip,test-app2.zip,2097152,https://example.com/test-app2-boxart.png,release/gba/test-app2-v1.1.gba,test-app2.gba +``` + +
+ +## Building +### Requirements +- devkitARM toolchain (`nds-dev` package group) by [devkitPro](https://github.com/devkitPro) +- [curl](https://github.com/ds-sloth/pacman-packages/tree/nds-curl-mbedtls/nds/curl) and [mbedtls](https://github.com/ds-sloth/pacman-packages/tree/nds-curl-mbedtls/nds/mbedtls) by [ds-sloth](https://github.com/ds-sloth) +- nds-zlib, nds-libpng from devkitPro pacman repository + +```sh +git clone https://github.com/cavv-dev/Kekatsu-DS.git +cd Kekatsu-DS +make release +``` + +## Credits +- [Cavv](https://github.com/cavv-dev): Main developer +- [devkitPro](https://github.com/devkitPro): devkitARM and relative libraries +- [ds-sloth](https://github.com/ds-sloth): curl and mbedtls libraries +- [Flaticon](https://www.flaticon.com/): Icons diff --git a/icon.bmp b/icon.bmp new file mode 100644 index 0000000..1efd60f Binary files /dev/null and b/icon.bmp differ diff --git a/resources/screenshots/Kekatsu-DS_1.png b/resources/screenshots/Kekatsu-DS_1.png new file mode 100644 index 0000000..814f927 Binary files /dev/null and b/resources/screenshots/Kekatsu-DS_1.png differ diff --git a/resources/screenshots/Kekatsu-DS_2.png b/resources/screenshots/Kekatsu-DS_2.png new file mode 100644 index 0000000..f4b3024 Binary files /dev/null and b/resources/screenshots/Kekatsu-DS_2.png differ diff --git a/resources/screenshots/Kekatsu-DS_3.png b/resources/screenshots/Kekatsu-DS_3.png new file mode 100644 index 0000000..792441f Binary files /dev/null and b/resources/screenshots/Kekatsu-DS_3.png differ diff --git a/source/archives.c b/source/archives.c new file mode 100644 index 0000000..f67f9b2 --- /dev/null +++ b/source/archives.c @@ -0,0 +1,158 @@ +#include "archives.h" + +#include "utils/filesystem.h" +#include "utils/strings.h" +#include +#include +#include + +bool fileIsZip(const char* filePath) +{ + FILE* fp = fopen(filePath, "rb"); + if (!fp) + return false; + + unsigned char buffer[4]; + size_t bytesRead = fread(buffer, 1, 4, fp); + fclose(fp); + + return (bytesRead == 4 && buffer[0] == 0x50 && buffer[1] == 0x4b && buffer[2] == 0x03 && buffer[3] == 0x04); +} + +ExtractStatus extractFile(unzFile zipFile, const char* outFilePath) +{ + char zipFilePath[PATH_MAX]; + if (unzGetCurrentFileInfo(zipFile, NULL, zipFilePath, sizeof(zipFilePath), NULL, 0, NULL, 0) != UNZ_OK) + return EXTRACT_ERR_FILE_READ; + + createDirStructure(outFilePath); + + if (zipFilePath[strlen(zipFilePath) - 1] == '/') { + if (createDir(outFilePath)) + return EXTRACT_SUCCESS; + else + return EXTRACT_ERR_FILE_WRITE; + } + + FILE* fp = fopen(outFilePath, "wb"); + if (!fp) + return EXTRACT_ERR_FILE_WRITE; + + char buffer[128]; + int bytesRead; + while ((bytesRead = unzReadCurrentFile(zipFile, buffer, sizeof(buffer))) > 0) { + if (fwrite(buffer, bytesRead, 1, fp) != 1) { + fclose(fp); + return EXTRACT_ERR_FILE_WRITE; + } + } + + fclose(fp); + return (bytesRead < 0) ? EXTRACT_ERR_FILE_READ : EXTRACT_SUCCESS; +} + +ExtractStatus extractZip(const char* filePath, const char* inPath, const char* outPath) +{ + unzFile zipFile = unzOpen(filePath); + if (!zipFile) + return EXTRACT_ERR_FILE_OPEN; + + if (inPath[strlen(inPath) - 1] == '/') { + if (unzGoToFirstFile(zipFile) != UNZ_OK) { + unzClose(zipFile); + return EXTRACT_ERR_FILE_READ; + } + + do { + char zipFilePath[PATH_MAX]; + if (unzGetCurrentFileInfo(zipFile, NULL, zipFilePath, sizeof(zipFilePath), NULL, 0, NULL, 0) != UNZ_OK) { + unzClose(zipFile); + return EXTRACT_ERR_FILE_READ; + } + + if (strncmp(zipFilePath, inPath, strlen(inPath)) != 0) + continue; + + char* outDirName = zipFilePath + strlen(inPath); + + if (strlen(outPath) + strlen(outDirName) > PATH_MAX - 1) + return EXTRACT_ERR_FILE_READ; + + char newOutPath[PATH_MAX]; + joinPath(newOutPath, outPath, outDirName); + if (unzOpenCurrentFile(zipFile) != UNZ_OK) { + unzClose(zipFile); + return EXTRACT_ERR_FILE_READ; + } + + ExtractStatus result = extractFile(zipFile, newOutPath); + unzCloseCurrentFile(zipFile); + if (result != EXTRACT_SUCCESS) { + unzClose(zipFile); + return result; + } + } while (unzGoToNextFile(zipFile) == UNZ_OK); + } else { + if (unzLocateFile(zipFile, inPath, 0) != UNZ_OK) { + unzClose(zipFile); + return EXTRACT_ERR_FILE_NOT_FOUND; + } + + if (unzOpenCurrentFile(zipFile) != UNZ_OK) { + unzClose(zipFile); + return EXTRACT_ERR_FILE_READ; + } + + ExtractStatus result = extractFile(zipFile, outPath); + unzCloseCurrentFile(zipFile); + if (result != EXTRACT_SUCCESS) { + unzClose(zipFile); + return result; + } + } + + unzClose(zipFile); + return EXTRACT_SUCCESS; +} + +ExtractStatus extractAllZip(const char* filePath, const char* outDir) +{ + unzFile zipFile = unzOpen(filePath); + if (!zipFile) + return EXTRACT_ERR_FILE_OPEN; + + if (unzGoToFirstFile(zipFile) != UNZ_OK) { + unzClose(zipFile); + return EXTRACT_ERR_FILE_READ; + } + + do { + if (unzOpenCurrentFile(zipFile) != UNZ_OK) { + unzClose(zipFile); + return EXTRACT_ERR_FILE_READ; + } + + char zipFilePath[PATH_MAX]; + if (unzGetCurrentFileInfo(zipFile, NULL, zipFilePath, sizeof(zipFilePath), NULL, 0, NULL, 0) != UNZ_OK) { + unzCloseCurrentFile(zipFile); + unzClose(zipFile); + return EXTRACT_ERR_FILE_READ; + } + + if (strlen(outDir) + strlen(zipFilePath) > PATH_MAX - 1) + return EXTRACT_ERR_FILE_READ; + + char outFilePath[PATH_MAX]; + joinPath(outFilePath, outDir, zipFilePath); + + ExtractStatus result = extractFile(zipFile, outFilePath); + unzCloseCurrentFile(zipFile); + if (result != EXTRACT_SUCCESS) { + unzClose(zipFile); + return result; + } + } while (unzGoToNextFile(zipFile) == UNZ_OK); + + unzClose(zipFile); + return EXTRACT_SUCCESS; +} diff --git a/source/archives.h b/source/archives.h new file mode 100644 index 0000000..dd55536 --- /dev/null +++ b/source/archives.h @@ -0,0 +1,14 @@ +#pragma once +#include + +typedef enum { + EXTRACT_SUCCESS, + EXTRACT_ERR_FILE_OPEN, + EXTRACT_ERR_FILE_NOT_FOUND, + EXTRACT_ERR_FILE_READ, + EXTRACT_ERR_FILE_WRITE +} ExtractStatus; + +bool fileIsZip(const char* filePath); +ExtractStatus extractZip(const char* filePath, const char* inFilePath, const char* outFilePath); +ExtractStatus extractAllZip(const char* filePath, const char* outDir); diff --git a/source/colors.c b/source/colors.c new file mode 100644 index 0000000..f9dd7a1 --- /dev/null +++ b/source/colors.c @@ -0,0 +1,54 @@ +#include "colors.h" + +u16 colorSchemes[COLOR_SCHEMES_COUNT][COLORS_COUNT]; + +void initColorSchemes(void) +{ + colorSchemes[COLOR_SCHEME_1][COLOR_BG] = RGB15_8BIT(9, 9, 15); + colorSchemes[COLOR_SCHEME_1][COLOR_BG_2] = RGB15_8BIT(20, 20, 33); + colorSchemes[COLOR_SCHEME_1][COLOR_TEXT] = RGB15_8BIT(168, 168, 237); + colorSchemes[COLOR_SCHEME_1][COLOR_TEXT_2] = RGB15_8BIT(243, 243, 243); + colorSchemes[COLOR_SCHEME_1][COLOR_TEXT_3] = RGB15_8BIT(194, 194, 194); + colorSchemes[COLOR_SCHEME_1][COLOR_PRIMARY] = RGB15_8BIT(110, 110, 186); + colorSchemes[COLOR_SCHEME_1][COLOR_SECONDARY] = RGB15_8BIT(55, 55, 105); + + colorSchemes[COLOR_SCHEME_2][COLOR_BG] = RGB15_8BIT(9, 12, 15); + colorSchemes[COLOR_SCHEME_2][COLOR_BG_2] = RGB15_8BIT(20, 26, 33); + colorSchemes[COLOR_SCHEME_2][COLOR_TEXT] = RGB15_8BIT(168, 197, 237); + colorSchemes[COLOR_SCHEME_2][COLOR_TEXT_2] = RGB15_8BIT(243, 243, 243); + colorSchemes[COLOR_SCHEME_2][COLOR_TEXT_3] = RGB15_8BIT(194, 194, 194); + colorSchemes[COLOR_SCHEME_2][COLOR_PRIMARY] = RGB15_8BIT(110, 142, 186); + colorSchemes[COLOR_SCHEME_2][COLOR_SECONDARY] = RGB15_8BIT(54, 75, 105); + + colorSchemes[COLOR_SCHEME_3][COLOR_BG] = RGB15_8BIT(15, 14, 9); + colorSchemes[COLOR_SCHEME_3][COLOR_BG_2] = RGB15_8BIT(33, 30, 20); + colorSchemes[COLOR_SCHEME_3][COLOR_TEXT] = RGB15_8BIT(237, 219, 168); + colorSchemes[COLOR_SCHEME_3][COLOR_TEXT_2] = RGB15_8BIT(243, 243, 243); + colorSchemes[COLOR_SCHEME_3][COLOR_TEXT_3] = RGB15_8BIT(194, 194, 194); + colorSchemes[COLOR_SCHEME_3][COLOR_PRIMARY] = RGB15_8BIT(186, 166, 110); + colorSchemes[COLOR_SCHEME_3][COLOR_SECONDARY] = RGB15_8BIT(105, 91, 54); + + colorSchemes[COLOR_SCHEME_4][COLOR_BG] = RGB15_8BIT(15, 9, 9); + colorSchemes[COLOR_SCHEME_4][COLOR_BG_2] = RGB15_8BIT(33, 20, 20); + colorSchemes[COLOR_SCHEME_4][COLOR_TEXT] = RGB15_8BIT(237, 168, 170); + colorSchemes[COLOR_SCHEME_4][COLOR_TEXT_2] = RGB15_8BIT(243, 243, 243); + colorSchemes[COLOR_SCHEME_4][COLOR_TEXT_3] = RGB15_8BIT(194, 194, 194); + colorSchemes[COLOR_SCHEME_4][COLOR_PRIMARY] = RGB15_8BIT(186, 110, 111); + colorSchemes[COLOR_SCHEME_4][COLOR_SECONDARY] = RGB15_8BIT(105, 54, 55); + + colorSchemes[COLOR_SCHEME_5][COLOR_BG] = RGB15_8BIT(9, 15, 12); + colorSchemes[COLOR_SCHEME_5][COLOR_BG_2] = RGB15_8BIT(20, 33, 25); + colorSchemes[COLOR_SCHEME_5][COLOR_TEXT] = RGB15_8BIT(168, 237, 196); + colorSchemes[COLOR_SCHEME_5][COLOR_TEXT_2] = RGB15_8BIT(243, 243, 243); + colorSchemes[COLOR_SCHEME_5][COLOR_TEXT_3] = RGB15_8BIT(194, 194, 194); + colorSchemes[COLOR_SCHEME_5][COLOR_PRIMARY] = RGB15_8BIT(110, 186, 140); + colorSchemes[COLOR_SCHEME_5][COLOR_SECONDARY] = RGB15_8BIT(54, 105, 74); + + colorSchemes[COLOR_SCHEME_6][COLOR_BG] = RGB15_8BIT(15, 15, 15); + colorSchemes[COLOR_SCHEME_6][COLOR_BG_2] = RGB15_8BIT(33, 33, 33); + colorSchemes[COLOR_SCHEME_6][COLOR_TEXT] = RGB15_8BIT(237, 237, 237); + colorSchemes[COLOR_SCHEME_6][COLOR_TEXT_2] = RGB15_8BIT(243, 243, 243); + colorSchemes[COLOR_SCHEME_6][COLOR_TEXT_3] = RGB15_8BIT(194, 194, 194); + colorSchemes[COLOR_SCHEME_6][COLOR_PRIMARY] = RGB15_8BIT(186, 186, 186); + colorSchemes[COLOR_SCHEME_6][COLOR_SECONDARY] = RGB15_8BIT(105, 105, 105); +} diff --git a/source/colors.h b/source/colors.h new file mode 100644 index 0000000..45acbb1 --- /dev/null +++ b/source/colors.h @@ -0,0 +1,29 @@ +#pragma once +#include + +typedef enum { + COLOR_SCHEME_1, + COLOR_SCHEME_2, + COLOR_SCHEME_3, + COLOR_SCHEME_4, + COLOR_SCHEME_5, + COLOR_SCHEME_6, + COLOR_SCHEMES_COUNT +} ColorSchemeEnum; + +typedef enum { + COLOR_BG, + COLOR_BG_2, + COLOR_TEXT, + COLOR_TEXT_2, + COLOR_TEXT_3, + COLOR_PRIMARY, + COLOR_SECONDARY, + COLORS_COUNT +} ColorEnum; + +#define RGB15_8BIT(r, g, b) (((r) >> 3) | (((g) >> 3) << 5) | (((b) >> 3) << 10)) + +extern u16 colorSchemes[COLOR_SCHEMES_COUNT][COLORS_COUNT]; + +void initColorSchemes(void); diff --git a/source/config.h b/source/config.h new file mode 100644 index 0000000..6bea912 --- /dev/null +++ b/source/config.h @@ -0,0 +1,9 @@ +#pragma once + +#define APP_NAME "Kekatsu" +#define APP_VERSION "1.0.0" +#define APPDATA_DIR "/" APP_NAME +#define CACHE_DIR APPDATA_DIR "/cache" + +#define UPDATE_URL_APP "https://github.com/cavv-dev/Kekatsu-DS/releases/latest/download/Kekatsu.nds" +#define UPDATE_URL_VERSION "https://github.com/cavv-dev/Kekatsu-DS/releases/latest/download/version.txt" diff --git a/source/database.c b/source/database.c new file mode 100644 index 0000000..890ee40 --- /dev/null +++ b/source/database.c @@ -0,0 +1,376 @@ +#include "database.h" + +#include "config.h" +#include "networking.h" +#include "utils/filesystem.h" +#include "utils/strings.h" +#include +#include + +#define TEMP_DB_FILENAME "db.txt" +#define TEMP_DB_PATH CACHE_DIR "/" TEMP_DB_FILENAME + +#define LAST_OPENED_DB_FILENAME "lastOpenedDb.txt" +#define LAST_OPENED_DB_PATH APPDATA_DIR "/" LAST_OPENED_DB_FILENAME + +#define DATABASE_LIST_FILENAME "databases.txt" +#define DATABASE_LIST_PATH APPDATA_DIR "/" DATABASE_LIST_FILENAME + +struct Database { + char* name; + char* value; + DatabaseType type; + char* path; + size_t size; + bool isInited; + FILE* fp; + char delimiter; +}; + +Database newDatabase(const char* name, const char* value) +{ + Database d = malloc(sizeof(struct Database)); + d->name = strdup(name); + d->value = strdup(value); + d->path = NULL; + d->size = 0; + d->isInited = false; + d->fp = NULL; + d->delimiter = '\0'; + + if ((strncmp(value, "http://", 7) == 0) || (strncmp(value, "https://", 8) == 0)) + d->type = DATABASE_TYPE_HTTP; + else + d->type = DATABASE_TYPE_LOCAL; + + return d; +} + +void freeDatabase(Database d) +{ + free(d->value); + free(d->path); + + if (d->fp) + closeDatabase(d); + + free(d); +} + +char* getDatabaseName(Database d) +{ + return d->name; +} + +char* getDatabaseValue(Database d) +{ + return d->value; +} + +char* getDatabasePath(Database d) +{ + return d->path; +} + +size_t getDatabaseSize(Database d) +{ + return d->size; +} + +bool getDatabaseIsInited(Database d) +{ + return d->isInited; +} + +char** parseLine(char* line, char delimiter, size_t* fieldsCount) +{ + if (line[0] == '\0') { + *fieldsCount = 0; + return NULL; + } + + // Count fields in line + *fieldsCount = 1; + for (char* ptr = line; *ptr != '\0'; ++ptr) { + if (*ptr == delimiter) + (*fieldsCount)++; + } + + // Allocate space for fields array + char** fields = (char**)malloc(*fieldsCount * sizeof(char*)); + if (fields == NULL) + return NULL; + + // Populate fields array with pointers to strings in line + size_t fieldIndex = 0; + char* start = line; + for (char* ptr = line;; ++ptr) { + if (*ptr == delimiter || *ptr == '\0') { + fields[fieldIndex] = start; + fieldIndex++; + if (*ptr == '\0') { + break; + } + *ptr = '\0'; // Replace delimiter with null terminator + start = ptr + 1; + } + } + + return fields; +} + +Database getLastOpenedDatabase(void) +{ + FILE* fp = fopen(LAST_OPENED_DB_PATH, "r"); + if (!fp) + return NULL; + + char line[2048]; + char name[1024]; + char value[1024]; + + if (!fgets(line, sizeof(line), fp) || sscanf(line, "%[^\t]\t%s", name, value) != 2) { + fclose(fp); + return NULL; + } + + fclose(fp); + + return newDatabase(name, value); +} + +void saveLastOpenedDatabase(Database d) +{ + FILE* fp = fopen(LAST_OPENED_DB_PATH, "w"); + if (!fp) + return; + + fprintf(fp, "%s\t%s\n", d->name, d->value); + fclose(fp); +} + +DatabaseInitStatus openDatabase(Database d) +{ + FILE* fp = fopen(d->path, "r"); + if (!fp) + return DATABASE_OPEN_ERR_FILE_OPEN; + + size_t dbSize = 0; + + char line[1024]; + if (!fgets(line, sizeof(line), fp)) { + fclose(fp); + return DATABASE_OPEN_ERR_INVALID_FORMAT; + } + + int dbVersion = atoi(line); + if (dbVersion != 1) { // 1 is the only supported version for now + fclose(fp); + return DATABASE_OPEN_ERR_INVALID_VERSION; + } + + if (!fgets(line, sizeof(line), fp)) { + fclose(fp); + return DATABASE_OPEN_ERR_INVALID_FORMAT; + } + + char delimiter = line[0]; + + while (fgets(line, sizeof(line), fp)) { + size_t fieldsCount; + char** fields = parseLine(line, delimiter, &fieldsCount); + + if (!fields || fieldsCount < 9) { + fclose(fp); + return DATABASE_OPEN_ERR_INVALID_FORMAT; + } + + // Calculate the fields count of items to extract + // Items are couple of fields so the count has to be an even number + size_t extractItemsFieldsCount = fieldsCount - 9; + if (fieldsCount > 9 && (extractItemsFieldsCount & 1) != 0) { + free(fields); + fclose(fp); + return DATABASE_OPEN_ERR_INVALID_FORMAT; + } + + dbSize++; + } + + d->size = dbSize; + d->fp = fp; + d->delimiter = delimiter; + + saveLastOpenedDatabase(d); + + return DATABASE_OPEN_SUCCESS; +} + +bool closeDatabase(Database d) +{ + if (fclose(d->fp) != 0) + return false; + + return true; +} + +DatabaseInitStatus initDatabase(Database d) +{ + bool usingCachedDb = false; + + switch (d->type) { + case DATABASE_TYPE_HTTP: + char* dbFileName = strdup(d->value); + safeStr(dbFileName); + + char dbFilePath[PATH_MAX]; + snprintf(dbFilePath, sizeof(dbFilePath), "%s/%s", CACHE_DIR, dbFileName); + free(dbFileName); + + d->path = strdup(dbFilePath); + + if (downloadFile(TEMP_DB_PATH, d->value, NULL) == DOWNLOAD_SUCCESS) { + if (!renamePath(TEMP_DB_PATH, dbFilePath)) + return DATABASE_INIT_ERR_DOWNLOAD; + } else { + if (fileExists(dbFilePath)) + usingCachedDb = true; + else + return DATABASE_INIT_ERR_DOWNLOAD; + } + + break; + case DATABASE_TYPE_LOCAL: + d->path = strdup(d->value); + + if (!pathExists(d->value)) + return DATABASE_INIT_ERR_FILE_NOT_FOUND; + + break; + } + + d->isInited = true; + + return (usingCachedDb ? DATABASE_INIT_SUCCESS_CACHE : DATABASE_INIT_SUCCESS); +} + +void alignDatabase(Database d) +{ + fseek(d->fp, 0, SEEK_SET); + + // Advance 2 lines + char ch; + u8 lines = 2; + while (lines > 0 && (ch = fgetc(d->fp)) != EOF) { + if (ch == '\n') { + lines--; + } + } +} + +Entry* searchDatabase(Database d, const char* searchTitle, size_t pageSize, size_t page, size_t* resultsCount) +{ + *resultsCount = 0; + size_t count = 0; + size_t capacity = 10; + size_t startIndex = (page - 1) * pageSize; + size_t endIndex = startIndex + pageSize; + size_t currIndex = 0; + char line[1024]; + Entry* results = malloc(capacity * sizeof(Entry)); + + alignDatabase(d); + + while (fgets(line, sizeof(line), d->fp)) { + // Trim trailing newline + line[strcspn(line, "\r\n")] = '\0'; + + size_t fieldsCount; + char** fields = parseLine(line, d->delimiter, &fieldsCount); + + if (searchTitle[0] != '\0') { + // Format titles in a comparable way + char tempEntryTitle[128]; + char tempInputTitle[128]; + + strncpy(tempEntryTitle, fields[0], sizeof(tempEntryTitle) - 1); + removeAccentsStr(tempEntryTitle); + lowerStr(tempEntryTitle); + + strncpy(tempInputTitle, searchTitle, sizeof(tempInputTitle) - 1); + lowerStr(tempInputTitle); + + if (!strstr(tempEntryTitle, tempInputTitle)) { + free(fields); + continue; + } + } + + if (currIndex < startIndex || currIndex >= endIndex) { + currIndex++; + continue; + } + + char** extractItemsFields = &fields[9]; // Items to extract start from the ninth field + + size_t extractItemsCount = (fieldsCount - 9) / 2; + struct EntryExtractItem extractItems[extractItemsCount]; + + for (size_t i = 0; i < extractItemsCount; i++) { + extractItems[i].outPath = extractItemsFields[i * 2]; + extractItems[i].inPath = extractItemsFields[i * 2 + 1]; + } + + Entry e = newEntry(fields[0], fields[1], fields[2], fields[3], fields[4], fields[5], fields[6], strtoull(fields[7], NULL, 10), fields[8], extractItems, extractItemsCount); + + // Increase the capacity if needed + if (count >= capacity) { + capacity *= 2; + results = realloc(results, capacity * sizeof(Entry)); + } + + results[count] = e; + count++; + + currIndex++; + free(fields); + + if (currIndex >= endIndex) + break; + } + + *resultsCount = count; + return results; +} + +Database* getDatabaseList(size_t* databasesCount) +{ + *databasesCount = 0; + + FILE* fp = fopen(DATABASE_LIST_PATH, "r"); + if (!fp) + return NULL; + + size_t count = 0; + size_t capacity = 8; + Database* databases = malloc(capacity * sizeof(Database)); + + char line[2048]; + char name[1024]; + char value[1024]; + while (fgets(line, sizeof(line), fp)) { + if (sscanf(line, "%[^\t]\t%s", name, value) != 2) + continue; + + if (count >= capacity) { + capacity *= 2; + databases = realloc(databases, capacity * sizeof(Database)); + } + + databases[count++] = newDatabase(name, value); + } + + fclose(fp); + *databasesCount = count; + return databases; +} diff --git a/source/database.h b/source/database.h new file mode 100644 index 0000000..a89fa3a --- /dev/null +++ b/source/database.h @@ -0,0 +1,39 @@ +#pragma once +#include "entries.h" +#include +#include + +typedef struct Database* Database; + +typedef enum { + DATABASE_TYPE_HTTP, + DATABASE_TYPE_LOCAL +} DatabaseType; + +typedef enum { + DATABASE_INIT_SUCCESS, + DATABASE_INIT_SUCCESS_CACHE, + DATABASE_INIT_ERR_DOWNLOAD, + DATABASE_INIT_ERR_FILE_NOT_FOUND +} DatabaseInitStatus; + +typedef enum { + DATABASE_OPEN_SUCCESS, + DATABASE_OPEN_ERR_FILE_OPEN, + DATABASE_OPEN_ERR_INVALID_VERSION, + DATABASE_OPEN_ERR_INVALID_FORMAT +} DatabaseOpenStatus; + +Database newDatabase(const char* name, const char* value); +void freeDatabase(Database); +char* getDatabaseName(Database); +char* getDatabaseValue(Database); +char* getDatabasePath(Database); +size_t getDatabaseSize(Database); +bool getDatabaseIsInited(Database); +Database getLastOpenedDatabase(void); +DatabaseInitStatus openDatabase(Database); +bool closeDatabase(Database d); +DatabaseInitStatus initDatabase(Database); +Entry* searchDatabase(Database, const char* searchTitle, size_t pageSize, size_t page, size_t* resultsCount); +Database* getDatabaseList(size_t* databasesCount); diff --git a/source/entries.c b/source/entries.c new file mode 100644 index 0000000..c6ec0e2 --- /dev/null +++ b/source/entries.c @@ -0,0 +1,120 @@ +#include "entries.h" + +#include +#include + +struct Entry { + char* title; + char* platform; + char* region; + char* version; + char* author; + char* url; + char* fileName; + u64 size; + char* boxartUrl; + struct EntryExtractItem* extractItems; + size_t extractItemsCount; +}; + +Entry newEntry(const char* title, const char* platform, char* region, const char* version, const char* author, const char* url, const char* fileName, u64 size, const char* boxartUrl, struct EntryExtractItem* extractItems, size_t extractItemsCount) +{ + Entry e = malloc(sizeof(struct Entry)); + e->title = strdup(title); + e->platform = strdup(platform); + e->region = strdup(region); + e->version = strdup(version); + e->author = strdup(author); + e->url = strdup(url); + e->fileName = strdup(fileName); + e->size = size; + e->boxartUrl = strdup(boxartUrl); + + e->extractItems = malloc(sizeof(struct EntryExtractItem) * extractItemsCount); + for (int i = 0; i < extractItemsCount; i++) { + e->extractItems[i].outPath = strdup(extractItems[i].outPath); + e->extractItems[i].inPath = strdup(extractItems[i].outPath); + } + + e->extractItemsCount = extractItemsCount; + + return e; +} + +void freeEntry(Entry e) +{ + free(e->title); + free(e->platform); + free(e->version); + free(e->author); + free(e->url); + free(e->boxartUrl); + + for (int i = 0; i < e->extractItemsCount; i++) { + free(e->extractItems[i].outPath); + free(e->extractItems[i].inPath); + } + + free(e->extractItems); + free(e); +} + +char* getEntryTitle(Entry e) +{ + return e->title; +} + +char* getEntryPlatform(Entry e) +{ + return e->platform; +} + +char* getEntryRegion(Entry e) +{ + return e->region; +} + +char* getEntryVersion(Entry e) +{ + return e->version; +} + +char* getEntryAuthor(Entry e) +{ + return e->author; +} + +char* getEntryUrl(Entry e) +{ + return e->url; +} + +char* getEntryFileName(Entry e) +{ + return e->fileName; +} + +u64 getEntrySize(Entry e) +{ + return e->size; +} + +char* getEntryBoxartUrl(Entry e) +{ + return e->boxartUrl; +} + +Entry cloneEntry(Entry e) +{ + return newEntry(e->title, e->platform, e->region, e->version, e->author, e->url, e->fileName, e->size, e->boxartUrl, e->extractItems, e->extractItemsCount); +} + +struct EntryExtractItem* getEntryExtractItems(Entry e) +{ + return e->extractItems; +} + +size_t getEntryExtractItemsCount(Entry e) +{ + return e->extractItemsCount; +} diff --git a/source/entries.h b/source/entries.h new file mode 100644 index 0000000..a18d65b --- /dev/null +++ b/source/entries.h @@ -0,0 +1,24 @@ +#pragma once +#include + +typedef struct Entry* Entry; + +struct EntryExtractItem { + char* outPath; + char* inPath; +}; + +Entry newEntry(const char* title, const char* platform, char* region, const char* version, const char* author, const char* url, const char* fileName, u64 size, const char* boxartUrl, struct EntryExtractItem* extractItems, size_t extractItemsCount); +void freeEntry(Entry); +char* getEntryTitle(Entry); +char* getEntryPlatform(Entry); +char* getEntryRegion(Entry); +char* getEntryVersion(Entry); +char* getEntryAuthor(Entry); +char* getEntryUrl(Entry); +char* getEntryFileName(Entry); +u64 getEntrySize(Entry); +char* getEntryBoxartUrl(Entry); +Entry cloneEntry(Entry); +struct EntryExtractItem* getEntryExtractItems(Entry); +size_t getEntryExtractItemsCount(Entry); diff --git a/source/gettext.c b/source/gettext.c new file mode 100644 index 0000000..88aadce --- /dev/null +++ b/source/gettext.c @@ -0,0 +1,249 @@ +// Based off https://github.com/dborth/libwiigui/blob/master/source/gettext.cpp + +#include "gettext.h" + +#include "en_lang.h" +#include "it_lang.h" +#include +#include +#include + +typedef struct _MSG { + unsigned long id; + char* msgstr; + struct _MSG* next; +} MSG; + +static MSG* baseMSG = 0; + +#define HASHWORDBITS 32 + +static inline unsigned long hashString(const char* strParam) +{ + unsigned long hval, g; + const char* str = strParam; + + hval = 0; + while (*str != '\0') { + hval <<= 4; + hval += (unsigned char)*str++; + g = hval & ((unsigned long)0xf << (HASHWORDBITS - 4)); + if (g != 0) { + hval ^= g >> (HASHWORDBITS - 8); + hval ^= g; + } + } + return hval; +} + +static char* expandEscape(const char* str) +{ + char *retval, *rp; + const char* cp = str; + + retval = (char*)malloc(strlen(str) + 1); + if (retval == NULL) + return NULL; + rp = retval; + + while (cp[0] != '\0' && cp[0] != '\\') + *rp++ = *cp++; + if (cp[0] != '\0') { + do { + switch (*++cp) { + case '\"': + *rp++ = '\"'; + ++cp; + break; + case 'a': + *rp++ = '\a'; + ++cp; + break; + case 'b': + *rp++ = '\b'; + ++cp; + break; + case 'f': + *rp++ = '\f'; + ++cp; + break; + case 'n': + *rp++ = '\n'; + ++cp; + break; + case 'r': + *rp++ = '\r'; + ++cp; + break; + case 't': + *rp++ = '\t'; + ++cp; + break; + case 'v': + *rp++ = '\v'; + ++cp; + break; + case '\\': + *rp = '\\'; + ++cp; + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': { + size_t ch = *cp++ - '0'; + if (*cp >= '0' && *cp <= '7') { + ch *= 8; + ch += *cp++ - '0'; + if (*cp >= '0' && *cp <= '7') { + ch *= 8; + ch += *cp++ - '0'; + } + } + *rp = ch; + } break; + default: + *rp = '\\'; + break; + } + while (cp[0] != '\0' && cp[0] != '\\') + *rp++ = *cp++; + } while (cp[0] != '\0'); + } + + *rp = '\0'; + return retval; +} + +static MSG* findMSG(unsigned long id) +{ + MSG* msg; + for (msg = baseMSG; msg; msg = msg->next) { + if (msg->id == id) + return msg; + } + return NULL; +} + +static MSG* setMSG(const char* msgid, const char* msgstr) +{ + unsigned long id = hashString(msgid); + MSG* msg = findMSG(id); + if (!msg) { + msg = (MSG*)malloc(sizeof(MSG)); + msg->id = id; + msg->msgstr = NULL; + msg->next = baseMSG; + baseMSG = msg; + } + if (msg) { + if (msgstr) { + if (msg->msgstr) + free(msg->msgstr); + msg->msgstr = expandEscape(msgstr); + } + return msg; + } + return NULL; +} + +static void gettextCleanUp(void) +{ + while (baseMSG) { + MSG* nextMsg = baseMSG->next; + free(baseMSG->msgstr); + free(baseMSG); + baseMSG = nextMsg; + } +} + +static char* memfgets(char* dst, size_t maxlen, char* src) +{ + if (!src || !dst || maxlen <= 0) + return NULL; + + char* newline = strchr(src, '\n'); + if (newline == NULL) + return NULL; + + memcpy(dst, src, (newline - src)); + dst[(newline - src)] = 0; + return ++newline; +} + +static bool loadLanguageFromMemory(const u8* lang_data, size_t lang_size) +{ + char line[256]; + char* lastID = NULL; + + const char *file, *eof; + + file = (const char*)lang_data; + eof = file + lang_size; + + gettextCleanUp(); + + while (file && file < eof) { + file = memfgets(line, sizeof(line), (char*)file); + + if (!file) + break; + + if (line[0] == '#') + continue; + + if (strncmp(line, "msgid \"", 7) == 0) { + char *msgid, *end; + if (lastID) { + free(lastID); + lastID = NULL; + } + msgid = &line[7]; + end = strrchr(msgid, '"'); + if (end && end - msgid > 1) { + *end = 0; + lastID = strdup(msgid); + } + } else if (strncmp(line, "msgstr \"", 8) == 0) { + char *msgstr, *end; + + if (lastID == NULL) + continue; + + msgstr = &line[8]; + end = strrchr(msgstr, '"'); + if (end && end - msgstr > 1) { + *end = 0; + setMSG(lastID, msgstr); + } + free(lastID); + lastID = NULL; + } + } + return true; +} + +void loadLanguage(LanguageEnum lang) +{ + switch (lang) { + case LANG_IT: + loadLanguageFromMemory(it_lang, it_lang_size); + break; + default: + loadLanguageFromMemory(en_lang, en_lang_size); + break; + } +} + +const char* gettext(const char* msgid) +{ + MSG* msg = findMSG(hashString(msgid)); + if (msg && msg->msgstr) + return msg->msgstr; + + return msgid; +} diff --git a/source/gettext.h b/source/gettext.h new file mode 100644 index 0000000..3e2a8ba --- /dev/null +++ b/source/gettext.h @@ -0,0 +1,10 @@ +#pragma once + +typedef enum { + LANG_EN, + LANG_IT, + LANGS_COUNT +} LanguageEnum; + +void loadLanguage(LanguageEnum lang); +const char* gettext(const char* msg); diff --git a/source/gfx/brickColor1.grit b/source/gfx/brickColor1.grit new file mode 100644 index 0000000..499b4d2 --- /dev/null +++ b/source/gfx/brickColor1.grit @@ -0,0 +1 @@ +-gb -gB8 -gTFF00FF diff --git a/source/gfx/brickColor1.png b/source/gfx/brickColor1.png new file mode 100644 index 0000000..5361848 Binary files /dev/null and b/source/gfx/brickColor1.png differ diff --git a/source/gfx/brickColor2.grit b/source/gfx/brickColor2.grit new file mode 100644 index 0000000..499b4d2 --- /dev/null +++ b/source/gfx/brickColor2.grit @@ -0,0 +1 @@ +-gb -gB8 -gTFF00FF diff --git a/source/gfx/brickColor2.png b/source/gfx/brickColor2.png new file mode 100644 index 0000000..9817d24 Binary files /dev/null and b/source/gfx/brickColor2.png differ diff --git a/source/gfx/lButton.grit b/source/gfx/lButton.grit new file mode 100644 index 0000000..499b4d2 --- /dev/null +++ b/source/gfx/lButton.grit @@ -0,0 +1 @@ +-gb -gB8 -gTFF00FF diff --git a/source/gfx/lButton.png b/source/gfx/lButton.png new file mode 100644 index 0000000..802c5be Binary files /dev/null and b/source/gfx/lButton.png differ diff --git a/source/gfx/navbarBrowseIcon.grit b/source/gfx/navbarBrowseIcon.grit new file mode 100644 index 0000000..499b4d2 --- /dev/null +++ b/source/gfx/navbarBrowseIcon.grit @@ -0,0 +1 @@ +-gb -gB8 -gTFF00FF diff --git a/source/gfx/navbarBrowseIcon.png b/source/gfx/navbarBrowseIcon.png new file mode 100644 index 0000000..fbb1bab Binary files /dev/null and b/source/gfx/navbarBrowseIcon.png differ diff --git a/source/gfx/navbarDatabasesIcon.grit b/source/gfx/navbarDatabasesIcon.grit new file mode 100644 index 0000000..499b4d2 --- /dev/null +++ b/source/gfx/navbarDatabasesIcon.grit @@ -0,0 +1 @@ +-gb -gB8 -gTFF00FF diff --git a/source/gfx/navbarDatabasesIcon.png b/source/gfx/navbarDatabasesIcon.png new file mode 100644 index 0000000..2e9a014 Binary files /dev/null and b/source/gfx/navbarDatabasesIcon.png differ diff --git a/source/gfx/navbarInfoIcon.grit b/source/gfx/navbarInfoIcon.grit new file mode 100644 index 0000000..499b4d2 --- /dev/null +++ b/source/gfx/navbarInfoIcon.grit @@ -0,0 +1 @@ +-gb -gB8 -gTFF00FF diff --git a/source/gfx/navbarInfoIcon.png b/source/gfx/navbarInfoIcon.png new file mode 100644 index 0000000..b78a0c4 Binary files /dev/null and b/source/gfx/navbarInfoIcon.png differ diff --git a/source/gfx/navbarSettingsIcon.grit b/source/gfx/navbarSettingsIcon.grit new file mode 100644 index 0000000..499b4d2 --- /dev/null +++ b/source/gfx/navbarSettingsIcon.grit @@ -0,0 +1 @@ +-gb -gB8 -gTFF00FF diff --git a/source/gfx/navbarSettingsIcon.png b/source/gfx/navbarSettingsIcon.png new file mode 100644 index 0000000..5562365 Binary files /dev/null and b/source/gfx/navbarSettingsIcon.png differ diff --git a/source/gfx/rButton.grit b/source/gfx/rButton.grit new file mode 100644 index 0000000..499b4d2 --- /dev/null +++ b/source/gfx/rButton.grit @@ -0,0 +1 @@ +-gb -gB8 -gTFF00FF diff --git a/source/gfx/rButton.png b/source/gfx/rButton.png new file mode 100644 index 0000000..536fdf1 Binary files /dev/null and b/source/gfx/rButton.png differ diff --git a/source/gui/box.c b/source/gui/box.c new file mode 100644 index 0000000..ee96e68 --- /dev/null +++ b/source/gui/box.c @@ -0,0 +1,93 @@ +#include "box.h" + +#include + +struct GuiBox { + size_t width; + size_t height; + size_t posX; + size_t posY; + u16 color; + size_t borderSize; + u16 borderColor; +}; + +GuiBox newGuiBox(size_t width, size_t height, u16 color) +{ + GuiBox gb = malloc(sizeof(struct GuiBox)); + gb->width = width; + gb->height = height; + gb->posX = 0; + gb->posY = 0; + gb->color = color; + gb->borderSize = 0; + gb->borderColor = 0; + + return gb; +} + +void freeGuiBox(GuiBox gb) +{ + free(gb); +} + +void setGuiBoxWidth(GuiBox gb, size_t width) +{ + gb->width = width; +} + +void setGuiBoxHeight(GuiBox gb, size_t height) +{ + gb->width = height; +} + +void setGuiBoxPos(GuiBox gb, size_t posX, size_t posY) +{ + gb->posX = posX; + gb->posY = posY; +} + +void setGuiBoxBorder(GuiBox gb, size_t borderSize, u16 borderColor) +{ + gb->borderSize = borderSize; + gb->borderColor = borderColor; +} + +void drawGuiBoxPos(GuiBox gb, size_t posX, size_t posY) +{ + if (gb->borderSize && gb->borderColor && gb->color) { + glBoxFilled( + posX, + posY, + posX + gb->width - 1, + posY + gb->height - 1, + gb->borderColor); + glBoxFilled( + posX + gb->borderSize, + posY + gb->borderSize, + posX + gb->width - gb->borderSize - 1, + posY + gb->height - gb->borderSize - 1, + gb->color); + } else if (gb->color) { + glBoxFilled( + posX, + posY, + posX + gb->width - 1, + posY + gb->height - 1, + gb->color); + } else if (gb->borderSize && gb->borderColor) { + for (size_t i = 0; i < gb->borderSize; i++) { + glBox( + posX + i, + posY + i, + posX + gb->width - i - 2, + posY + gb->height - i - 2, + gb->borderColor); + } + } +} + +void drawGuiBox(GuiBox gb) +{ + drawGuiBoxPos(gb, gb->posX, gb->posY); +} diff --git a/source/gui/box.h b/source/gui/box.h new file mode 100644 index 0000000..afbe55b --- /dev/null +++ b/source/gui/box.h @@ -0,0 +1,13 @@ +#pragma once +#include + +typedef struct GuiBox* GuiBox; + +GuiBox newGuiBox(size_t width, size_t height, u16 color); +void freeGuiBox(GuiBox gb); +void setGuiBoxWidth(GuiBox, size_t); +void setGuiBoxHeight(GuiBox, size_t); +void setGuiBoxPos(GuiBox, size_t posX, size_t posY); +void setGuiBoxBorder(GuiBox, size_t borderSize, u16 borderColor); +void drawGuiBoxPos(GuiBox, size_t posX, size_t posY); +void drawGuiBox(GuiBox); diff --git a/source/gui/button.c b/source/gui/button.c new file mode 100644 index 0000000..5e50ba2 --- /dev/null +++ b/source/gui/button.c @@ -0,0 +1,161 @@ +#include "button.h" + +#include "input.h" +#include + +struct GuiButton { + size_t width; + size_t height; + size_t posX; + size_t posY; + GuiButtonState state; + GuiBox bg; + GuiBox bgHover; + GuiText label; + GuiImage icon; + GuiImage iconHover; +}; + +GuiButton newGuiButton(size_t width, size_t height) +{ + GuiButton gb = malloc(sizeof(struct GuiButton)); + gb->width = width; + gb->height = height; + gb->posX = 0; + gb->posY = 0; + gb->state = GUI_BUTTON_STATE_DEFAULT; + gb->bg = NULL; + gb->bgHover = NULL; + gb->label = NULL; + gb->icon = NULL; + gb->iconHover = NULL; + + return gb; +} + +void freeGuiButton(GuiButton gb) +{ + free(gb); +} + +void setGuiButtonWidth(GuiButton gb, size_t width) +{ + gb->width = width; +} + +void setGuiButtonHeight(GuiButton gb, size_t height) +{ + gb->height = height; +} + +void setGuiButtonPos(GuiButton gb, size_t posX, size_t posY) +{ + gb->posX = posX; + gb->posY = posY; +} + +void setGuiButtonState(GuiButton gb, GuiButtonState state) +{ + gb->state = state; +} + +void resetGuiButtonState(GuiButton gb) +{ + gb->state = GUI_BUTTON_STATE_DEFAULT; +} + +void setGuiButtonBg(GuiButton gb, GuiBox bg, GuiBox bgHover) +{ + gb->bg = bg; + gb->bgHover = bgHover; +} + +void setGuiButtonLabel(GuiButton gb, GuiText label) +{ + gb->label = label; +} + +void setGuiButtonIcon(GuiButton gb, GuiImage icon, GuiImage iconHover) +{ + gb->icon = icon; + gb->iconHover = iconHover; +} + +size_t getGuiButtonPosX(GuiButton gb) +{ + return gb->posX; +} + +size_t getGuiButtonPosY(GuiButton gb) +{ + return gb->posY; +} + +GuiButtonState getGuiButtonState(GuiButton gb) +{ + return gb->state; +} + +void handleTouchGuiButton(GuiButton gb) +{ + if ((gb->state == GUI_BUTTON_STATE_HELD) && (touch.px == 0) && (touch.py == 0)) + gb->state = GUI_BUTTON_STATE_CLICKED; + + if (!touch.px & !touch.py) + return; + + if (((touch.px >= gb->posX) && (touch.px <= (gb->posX + gb->width))) && ((touch.py >= gb->posY) && (touch.py <= (gb->posY + gb->height)))) + gb->state = GUI_BUTTON_STATE_HELD; + else if (gb->state == GUI_BUTTON_STATE_HELD) + gb->state = GUI_BUTTON_STATE_DEFAULT; +} + +void drawGuiButtonPos(GuiButton gb, size_t posX, size_t posY) +{ + if (gb->bg && (gb->state == GUI_BUTTON_STATE_DEFAULT || gb->state == GUI_BUTTON_STATE_CLICKED)) + drawGuiBoxPos(gb->bg, posX, posY); + else if (gb->bgHover && (gb->state == GUI_BUTTON_STATE_HELD || gb->state == GUI_BUTTON_STATE_SELECTED)) + drawGuiBoxPos(gb->bgHover, posX, posY); + + if (gb->icon && (gb->state == GUI_BUTTON_STATE_DEFAULT || gb->state == GUI_BUTTON_STATE_CLICKED)) + drawGuiImagePos(gb->icon, posX + (gb->width - getGuiImageWidth(gb->icon)) / 2, posY + (gb->height - getGuiImageHeight(gb->icon)) / 2); + + if (gb->iconHover && (gb->state == GUI_BUTTON_STATE_HELD || gb->state == GUI_BUTTON_STATE_SELECTED)) + drawGuiImagePos(gb->iconHover, posX + (gb->width - getGuiImageWidth(gb->iconHover)) / 2, posY + (gb->height - getGuiImageHeight(gb->iconHover)) / 2); + + if (gb->label) { + size_t labelPosX; + size_t labelPosY; + + switch (getGuiTextHAlignment(gb->label)) { + case GUI_TEXT_H_ALIGN_CENTER: + labelPosX = getGuiTextPosX(gb->label) + posX + gb->width / 2; + break; + case GUI_TEXT_H_ALIGN_RIGHT: + labelPosX = getGuiTextPosX(gb->label) + posX + gb->width; + break; + default: + labelPosX = getGuiTextPosX(gb->label) + posX; + break; + } + + switch (getGuiTextVAlignment(gb->label)) { + case GUI_TEXT_V_ALIGN_MIDDLE: + labelPosY = getGuiTextPosY(gb->label) + posY + gb->height / 2; + break; + case GUI_TEXT_V_ALIGN_BOTTOM: + labelPosY = getGuiTextPosY(gb->label) + posY + getGuiTextHeight(gb->label); + break; + default: + labelPosY = getGuiTextPosY(gb->label) + posY; + break; + } + + drawGuiTextPos(gb->label, labelPosX, labelPosY); + } +} + +void drawGuiButton(GuiButton gb) +{ + drawGuiButtonPos(gb, gb->posX, gb->posY); +} diff --git a/source/gui/button.h b/source/gui/button.h new file mode 100644 index 0000000..9a59e80 --- /dev/null +++ b/source/gui/button.h @@ -0,0 +1,29 @@ +#pragma once +#include "box.h" +#include "image.h" +#include "text.h" + +typedef struct GuiButton* GuiButton; + +typedef enum { + GUI_BUTTON_STATE_DEFAULT, + GUI_BUTTON_STATE_HELD, + GUI_BUTTON_STATE_SELECTED, + GUI_BUTTON_STATE_CLICKED +} GuiButtonState; + +GuiButton newGuiButton(size_t width, size_t height); +void freeGuiButton(GuiButton); +void setGuiButtonWidth(GuiButton, size_t); +void setGuiButtonPos(GuiButton, size_t posX, size_t posY); +void setGuiButtonState(GuiButton, GuiButtonState); +void resetGuiButtonState(GuiButton); +void setGuiButtonBg(GuiButton, GuiBox bg, GuiBox bgHover); +void setGuiButtonLabel(GuiButton, GuiText); +void setGuiButtonIcon(GuiButton, GuiImage icon, GuiImage iconHover); +size_t getGuiButtonPosX(GuiButton); +size_t getGuiButtonPosY(GuiButton); +GuiButtonState getGuiButtonState(GuiButton); +void handleTouchGuiButton(GuiButton); +void drawGuiButtonPos(GuiButton, size_t posX, size_t posY); +void drawGuiButton(GuiButton); diff --git a/source/gui/gfx/backspaceKey.grit b/source/gui/gfx/backspaceKey.grit new file mode 100644 index 0000000..499b4d2 --- /dev/null +++ b/source/gui/gfx/backspaceKey.grit @@ -0,0 +1 @@ +-gb -gB8 -gTFF00FF diff --git a/source/gui/gfx/backspaceKey.png b/source/gui/gfx/backspaceKey.png new file mode 100644 index 0000000..550665e Binary files /dev/null and b/source/gui/gfx/backspaceKey.png differ diff --git a/source/gui/gfx/fontBigUVCoords.h b/source/gui/gfx/fontBigUVCoords.h new file mode 100644 index 0000000..f60cc6a --- /dev/null +++ b/source/gui/gfx/fontBigUVCoords.h @@ -0,0 +1,265 @@ +// Generated by bmfont-to-spriteset (https://github.com/cavv-dev/bmfont-to-spriteset) + +#pragma once +#define FONTBIG_BITMAP_WIDTH 256 +#define FONTBIG_BITMAP_HEIGHT 256 +#define FONTBIG_NUM_IMAGES 256 + +const unsigned int fontBigTexCoords[] = { + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 16, 168, 3, 20, // + 4, 168, 3, 20, // ! + 24, 147, 7, 20, // " + 68, 63, 10, 20, // # + 120, 84, 9, 20, // $ + 140, 0, 15, 20, // % + 101, 42, 11, 20, // & + 252, 126, 3, 20, // ' + 147, 147, 5, 20, // ( + 195, 147, 5, 20, // ) + 40, 147, 7, 20, // * + 208, 126, 8, 20, // + + 48, 168, 3, 20, // , + 236, 147, 4, 20, // - + 40, 168, 3, 20, // . + 171, 147, 5, 20, // / + 190, 126, 8, 20, // 0 + 150, 84, 9, 20, // 1 + 181, 126, 8, 20, // 2 + 172, 126, 8, 20, // 3 + 101, 63, 10, 20, // 4 + 163, 126, 8, 20, // 5 + 154, 126, 8, 20, // 6 + 145, 126, 8, 20, // 7 + 136, 126, 8, 20, // 8 + 118, 126, 8, 20, // 9 + 32, 168, 3, 20, // : + 36, 168, 3, 20, // ; + 180, 84, 9, 20, // < + 190, 84, 9, 20, // = + 200, 84, 9, 20, // > + 210, 84, 9, 20, // ? + 74, 0, 16, 20, // @ + 70, 21, 13, 20, // A + 137, 42, 11, 20, // B + 173, 42, 11, 20, // C + 161, 42, 11, 20, // D + 134, 63, 10, 20, // E + 220, 84, 9, 20, // F + 152, 21, 12, 20, // G + 125, 42, 11, 20, // H + 56, 168, 3, 20, // I + 240, 84, 9, 20, // J + 165, 21, 12, 20, // K + 0, 105, 9, 20, // L + 14, 21, 13, 20, // M + 185, 42, 11, 20, // N + 217, 21, 12, 20, // O + 57, 63, 10, 20, // P + 139, 21, 12, 20, // Q + 191, 21, 12, 20, // R + 90, 63, 10, 20, // S + 60, 105, 9, 20, // T + 89, 42, 11, 20, // U + 149, 42, 11, 20, // V + 56, 0, 17, 20, // W + 243, 21, 12, 20, // X + 77, 42, 11, 20, // Y + 100, 126, 8, 20, // Z + 165, 147, 5, 20, // [ + 153, 147, 5, 20, // Backslash + 183, 147, 5, 20, // ] + 40, 84, 9, 20, // ^ + 35, 63, 10, 20, // _ + 211, 147, 4, 20, // ` + 178, 63, 9, 20, // a + 188, 63, 9, 20, // b + 37, 126, 8, 20, // c + 198, 63, 9, 20, // d + 208, 63, 9, 20, // e + 91, 147, 6, 20, // f + 218, 63, 9, 20, // g + 228, 63, 9, 20, // h + 20, 168, 3, 20, // i + 250, 105, 5, 20, // j + 238, 63, 9, 20, // k + 12, 168, 3, 20, // l + 156, 0, 15, 20, // m + 0, 84, 9, 20, // n + 10, 84, 9, 20, // o + 20, 84, 9, 20, // p + 30, 84, 9, 20, // q + 126, 147, 6, 20, // r + 247, 0, 8, 20, // s + 98, 147, 6, 20, // t + 50, 84, 9, 20, // u + 60, 84, 9, 20, // v + 28, 21, 13, 20, // w + 70, 84, 9, 20, // x + 80, 84, 9, 20, // y + 48, 147, 7, 20, // z + 16, 147, 7, 20, // { + 60, 168, 2, 20, // | + 32, 147, 7, 20, // } + 90, 84, 9, 20, // ~ + 210, 84, 9, 20, // ? + 100, 84, 9, 20, // € + 210, 84, 9, 20, // ? + 52, 168, 3, 20, // ‚ + 19, 126, 8, 20, // ƒ + 248, 63, 7, 20, // „ + 124, 0, 15, 20, // … + 46, 126, 8, 20, // † + 55, 126, 8, 20, // ‡ + 105, 147, 6, 20, // ˆ + 0, 0, 18, 20, // ‰ + 112, 63, 10, 20, // Š + 216, 147, 4, 20, // ‹ + 91, 0, 16, 20, // Œ + 210, 84, 9, 20, // ? + 127, 126, 8, 20, // Ž + 210, 84, 9, 20, // ? + 210, 84, 9, 20, // ? + 8, 168, 3, 20, // ‘ + 44, 168, 3, 20, // ’ + 0, 147, 7, 20, // “ + 8, 147, 7, 20, // ” + 189, 147, 5, 20, // • + 10, 126, 8, 20, // – + 19, 0, 18, 20, // — + 133, 147, 6, 20, // ˜ + 108, 0, 15, 20, // ™ + 64, 126, 8, 20, // š + 206, 147, 4, 20, // › + 172, 0, 15, 20, // œ + 210, 84, 9, 20, // ? + 244, 126, 7, 20, // ž + 197, 42, 11, 20, // Ÿ + 24, 168, 3, 20, //   + 28, 168, 3, 20, // ¡ + 73, 126, 8, 20, // ¢ + 82, 126, 8, 20, // £ + 167, 63, 10, 20, // ¤ + 230, 84, 9, 20, // ¥ + 63, 168, 2, 20, // ¦ + 109, 126, 8, 20, // § + 140, 147, 6, 20, // ¨ + 204, 21, 12, 20, // © + 56, 147, 6, 20, // ª + 199, 126, 8, 20, // « + 140, 84, 9, 20, // ¬ + 241, 147, 4, 20, // ­ + 178, 21, 12, 20, // ® + 123, 63, 10, 20, // ¯ + 77, 147, 6, 20, // ° + 217, 126, 8, 20, // ± + 159, 147, 5, 20, // ² + 250, 84, 5, 20, // ³ + 201, 147, 4, 20, // ´ + 226, 126, 8, 20, // µ + 130, 84, 9, 20, // ¶ + 251, 147, 3, 20, // · + 0, 168, 3, 20, // ¸ + 84, 147, 6, 20, // ¹ + 177, 147, 5, 20, // º + 28, 126, 8, 20, // » + 188, 0, 14, 20, // ¼ + 218, 0, 14, 20, // ½ + 0, 21, 13, 20, // ¾ + 110, 84, 9, 20, // ¿ + 233, 0, 13, 20, // À + 112, 21, 13, 20, // Á + 98, 21, 13, 20, //  + 84, 21, 13, 20, // à + 56, 21, 13, 20, // Ä + 42, 21, 13, 20, // Å + 38, 0, 17, 20, // Æ + 113, 42, 11, 20, // Ç + 245, 42, 10, 20, // È + 24, 63, 10, 20, // É + 46, 63, 10, 20, // Ê + 79, 63, 10, 20, // Ë + 221, 147, 4, 20, // Ì + 226, 147, 4, 20, // Í + 63, 147, 6, 20, // Î + 70, 147, 6, 20, // Ï + 126, 21, 12, 20, // Ð + 65, 42, 11, 20, // Ñ + 0, 42, 12, 20, // Ò + 13, 42, 12, 20, // Ó + 26, 42, 12, 20, // Ô + 39, 42, 12, 20, // Õ + 52, 42, 12, 20, // Ö + 170, 84, 9, 20, // × + 230, 21, 12, 20, // Ø + 209, 42, 11, 20, // Ù + 221, 42, 11, 20, // Ú + 233, 42, 11, 20, // Û + 0, 63, 11, 20, // Ü + 12, 63, 11, 20, // Ý + 145, 63, 10, 20, // Þ + 156, 63, 10, 20, // ß + 10, 105, 9, 20, // à + 20, 105, 9, 20, // á + 30, 105, 9, 20, // â + 40, 105, 9, 20, // ã + 160, 84, 9, 20, // ä + 50, 105, 9, 20, // å + 203, 0, 14, 20, // æ + 91, 126, 8, 20, // ç + 80, 105, 9, 20, // è + 90, 105, 9, 20, // é + 100, 105, 9, 20, // ê + 110, 105, 9, 20, // ë + 246, 147, 4, 20, // ì + 231, 147, 4, 20, // í + 112, 147, 6, 20, // î + 119, 147, 6, 20, // ï + 120, 105, 9, 20, // ð + 130, 105, 9, 20, // ñ + 140, 105, 9, 20, // ò + 150, 105, 9, 20, // ó + 160, 105, 9, 20, // ô + 170, 105, 9, 20, // õ + 180, 105, 9, 20, // ö + 235, 126, 8, 20, // ÷ + 190, 105, 9, 20, // ø + 200, 105, 9, 20, // ù + 210, 105, 9, 20, // ú + 220, 105, 9, 20, // û + 230, 105, 9, 20, // ü + 240, 105, 9, 20, // ý + 0, 126, 9, 20, // þ + 70, 105, 9, 20, // ÿ + 210, 84, 9, 20, // ? +}; diff --git a/source/gui/gfx/fontBig_0.grit b/source/gui/gfx/fontBig_0.grit new file mode 100644 index 0000000..384c666 --- /dev/null +++ b/source/gui/gfx/fontBig_0.grit @@ -0,0 +1 @@ +-gb -gB8 diff --git a/source/gui/gfx/fontBig_0.png b/source/gui/gfx/fontBig_0.png new file mode 100644 index 0000000..6157f9b Binary files /dev/null and b/source/gui/gfx/fontBig_0.png differ diff --git a/source/gui/gfx/fontMediumUVCoords.h b/source/gui/gfx/fontMediumUVCoords.h new file mode 100644 index 0000000..c1d4ab3 --- /dev/null +++ b/source/gui/gfx/fontMediumUVCoords.h @@ -0,0 +1,265 @@ +// Generated by bmfont-to-spriteset (https://github.com/cavv-dev/bmfont-to-spriteset) + +#pragma once +#define FONTMEDIUM_BITMAP_WIDTH 256 +#define FONTMEDIUM_BITMAP_HEIGHT 256 +#define FONTMEDIUM_NUM_IMAGES 256 + +const unsigned int fontMediumTexCoords[] = { + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 252, 68, 3, 16, // + 64, 102, 2, 16, // ! + 187, 85, 5, 16, // " + 135, 34, 8, 16, // # + 249, 34, 6, 16, // $ + 179, 0, 10, 16, // % + 45, 34, 8, 16, // & + 100, 102, 2, 16, // ' + 221, 85, 4, 16, // ( + 241, 85, 4, 16, // ) + 193, 85, 5, 16, // * + 36, 34, 8, 16, // + + 85, 102, 2, 16, // , + 252, 17, 3, 16, // - + 82, 102, 2, 16, // . + 211, 85, 4, 16, // / + 49, 85, 6, 16, // 0 + 42, 85, 6, 16, // 1 + 35, 85, 6, 16, // 2 + 168, 68, 6, 16, // 3 + 152, 51, 7, 16, // 4 + 28, 85, 6, 16, // 5 + 245, 68, 6, 16, // 6 + 238, 68, 6, 16, // 7 + 231, 68, 6, 16, // 8 + 224, 68, 6, 16, // 9 + 97, 102, 2, 16, // : + 88, 102, 2, 16, // ; + 210, 68, 6, 16, // < + 189, 68, 6, 16, // = + 182, 68, 6, 16, // > + 40, 68, 7, 16, // ? + 84, 0, 12, 16, // @ + 120, 17, 9, 16, // A + 81, 34, 8, 16, // B + 216, 17, 8, 16, // C + 18, 34, 8, 16, // D + 153, 34, 7, 16, // E + 144, 51, 7, 16, // F + 130, 17, 9, 16, // G + 189, 17, 8, 16, // H + 94, 102, 2, 16, // I + 169, 34, 7, 16, // J + 140, 17, 9, 16, // K + 140, 68, 6, 16, // L + 121, 0, 11, 16, // M + 243, 17, 8, 16, // N + 170, 17, 9, 16, // O + 177, 34, 7, 16, // P + 150, 17, 9, 16, // Q + 27, 34, 8, 16, // R + 185, 34, 7, 16, // S + 119, 68, 6, 16, // T + 54, 34, 8, 16, // U + 222, 0, 9, 16, // V + 44, 0, 13, 16, // W + 212, 0, 9, 16, // X + 90, 34, 8, 16, // Y + 21, 85, 6, 16, // Z + 0, 102, 4, 16, // [ + 251, 85, 4, 16, // Backslash + 246, 85, 4, 16, // ] + 0, 85, 6, 16, // ^ + 63, 34, 8, 16, // _ + 38, 102, 3, 16, // ` + 201, 34, 7, 16, // a + 209, 34, 7, 16, // b + 7, 85, 6, 16, // c + 217, 34, 7, 16, // d + 225, 34, 7, 16, // e + 199, 85, 5, 16, // f + 241, 34, 7, 16, // g + 0, 51, 7, 16, // h + 61, 102, 2, 16, // i + 30, 102, 3, 16, // j + 8, 51, 7, 16, // k + 76, 102, 2, 16, // l + 201, 0, 10, 16, // m + 193, 34, 7, 16, // n + 24, 51, 7, 16, // o + 32, 51, 7, 16, // p + 40, 51, 7, 16, // q + 133, 85, 5, 16, // r + 133, 68, 6, 16, // s + 205, 85, 5, 16, // t + 48, 51, 7, 16, // u + 56, 51, 7, 16, // v + 145, 0, 11, 16, // w + 72, 34, 8, 16, // x + 64, 51, 7, 16, // y + 91, 85, 5, 16, // z + 97, 85, 5, 16, // { + 70, 102, 2, 16, // | + 115, 85, 5, 16, // } + 126, 68, 6, 16, // ~ + 40, 68, 7, 16, // ? + 80, 51, 7, 16, // € + 40, 68, 7, 16, // ? + 67, 102, 2, 16, // ‚ + 154, 68, 6, 16, // ƒ + 121, 85, 5, 16, // „ + 168, 0, 10, 16, // … + 112, 68, 6, 16, // † + 84, 85, 6, 16, // ‡ + 127, 85, 5, 16, // ˆ + 71, 0, 12, 16, // ‰ + 72, 51, 7, 16, // Š + 50, 102, 3, 16, // ‹ + 30, 0, 13, 16, // Œ + 40, 68, 7, 16, // ? + 161, 68, 6, 16, // Ž + 40, 68, 7, 16, // ? + 40, 68, 7, 16, // ? + 79, 102, 2, 16, // ‘ + 103, 102, 2, 16, // ’ + 139, 85, 5, 16, // “ + 145, 85, 5, 16, // ” + 5, 102, 4, 16, // • + 233, 34, 7, 16, // – + 0, 0, 14, 16, // — + 151, 85, 5, 16, // ˜ + 58, 0, 12, 16, // ™ + 77, 85, 6, 16, // š + 10, 102, 3, 16, // › + 133, 0, 11, 16, // œ + 40, 68, 7, 16, // ? + 169, 85, 5, 16, // ž + 198, 17, 8, 16, // Ÿ + 34, 102, 3, 16, //   + 58, 102, 2, 16, // ¡ + 14, 85, 6, 16, // ¢ + 175, 68, 6, 16, // £ + 225, 17, 8, 16, // ¤ + 234, 17, 8, 16, // ¥ + 73, 102, 2, 16, // ¦ + 203, 68, 6, 16, // § + 175, 85, 5, 16, // ¨ + 160, 17, 9, 16, // © + 181, 85, 5, 16, // ª + 161, 34, 7, 16, // « + 217, 68, 6, 16, // ¬ + 54, 102, 3, 16, // ­ + 232, 0, 9, 16, // ® + 9, 34, 8, 16, // ¯ + 216, 85, 4, 16, // ° + 56, 85, 6, 16, // ± + 226, 85, 4, 16, // ² + 231, 85, 4, 16, // ³ + 252, 0, 3, 16, // ´ + 63, 85, 6, 16, // µ + 70, 85, 6, 16, // ¶ + 91, 102, 2, 16, // · + 14, 102, 3, 16, // ¸ + 18, 102, 3, 16, // ¹ + 236, 85, 4, 16, // º + 136, 51, 7, 16, // » + 157, 0, 10, 16, // ¼ + 190, 0, 10, 16, // ½ + 97, 0, 11, 16, // ¾ + 88, 51, 7, 16, // ¿ + 100, 17, 9, 16, // À + 90, 17, 9, 16, // Á + 80, 17, 9, 16, //  + 70, 17, 9, 16, // à + 60, 17, 9, 16, // Ä + 110, 17, 9, 16, // Å + 15, 0, 14, 16, // Æ + 207, 17, 8, 16, // Ç + 96, 51, 7, 16, // È + 104, 51, 7, 16, // É + 112, 51, 7, 16, // Ê + 120, 51, 7, 16, // Ë + 42, 102, 3, 16, // Ì + 46, 102, 3, 16, // Í + 157, 85, 5, 16, // Î + 163, 85, 5, 16, // Ï + 50, 17, 9, 16, // Ð + 0, 34, 8, 16, // Ñ + 40, 17, 9, 16, // Ò + 30, 17, 9, 16, // Ó + 20, 17, 9, 16, // Ô + 10, 17, 9, 16, // Õ + 0, 17, 9, 16, // Ö + 128, 51, 7, 16, // × + 242, 0, 9, 16, // Ø + 99, 34, 8, 16, // Ù + 180, 17, 8, 16, // Ú + 108, 34, 8, 16, // Û + 117, 34, 8, 16, // Ü + 126, 34, 8, 16, // Ý + 160, 51, 7, 16, // Þ + 147, 68, 6, 16, // ß + 168, 51, 7, 16, // à + 176, 51, 7, 16, // á + 184, 51, 7, 16, // â + 192, 51, 7, 16, // ã + 208, 51, 7, 16, // ä + 200, 51, 7, 16, // å + 109, 0, 11, 16, // æ + 196, 68, 6, 16, // ç + 16, 51, 7, 16, // è + 216, 51, 7, 16, // é + 224, 51, 7, 16, // ê + 232, 51, 7, 16, // ë + 22, 102, 3, 16, // ì + 26, 102, 3, 16, // í + 103, 85, 5, 16, // î + 109, 85, 5, 16, // ï + 240, 51, 7, 16, // ð + 248, 51, 7, 16, // ñ + 0, 68, 7, 16, // ò + 8, 68, 7, 16, // ó + 16, 68, 7, 16, // ô + 24, 68, 7, 16, // õ + 32, 68, 7, 16, // ö + 144, 34, 8, 16, // ÷ + 48, 68, 7, 16, // ø + 56, 68, 7, 16, // ù + 64, 68, 7, 16, // ú + 72, 68, 7, 16, // û + 80, 68, 7, 16, // ü + 88, 68, 7, 16, // ý + 96, 68, 7, 16, // þ + 104, 68, 7, 16, // ÿ + 40, 68, 7, 16, // ? +}; diff --git a/source/gui/gfx/fontMedium_0.grit b/source/gui/gfx/fontMedium_0.grit new file mode 100644 index 0000000..384c666 --- /dev/null +++ b/source/gui/gfx/fontMedium_0.grit @@ -0,0 +1 @@ +-gb -gB8 diff --git a/source/gui/gfx/fontMedium_0.png b/source/gui/gfx/fontMedium_0.png new file mode 100644 index 0000000..cf54ee3 Binary files /dev/null and b/source/gui/gfx/fontMedium_0.png differ diff --git a/source/gui/gfx/fontSmallUVCoords.h b/source/gui/gfx/fontSmallUVCoords.h new file mode 100644 index 0000000..d0f0f5f --- /dev/null +++ b/source/gui/gfx/fontSmallUVCoords.h @@ -0,0 +1,265 @@ +// Generated by bmfont-to-spriteset (https://github.com/cavv-dev/bmfont-to-spriteset) + +#pragma once +#define FONTSMALL_BITMAP_WIDTH 256 +#define FONTSMALL_BITMAP_HEIGHT 256 +#define FONTSMALL_NUM_IMAGES 256 + +const unsigned int fontSmallTexCoords[] = { + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 8, 65, 3, 12, // + 55, 65, 2, 12, // ! + 195, 39, 5, 12, // " + 201, 39, 5, 12, // # + 207, 39, 5, 12, // $ + 147, 0, 8, 12, // % + 230, 0, 7, 12, // & + 46, 65, 2, 12, // ' + 236, 52, 3, 12, // ( + 220, 52, 3, 12, // ) + 194, 52, 4, 12, // * + 6, 52, 5, 12, // + + 43, 65, 2, 12, // , + 252, 26, 3, 12, // - + 79, 65, 2, 12, // . + 24, 65, 3, 12, // / + 36, 52, 5, 12, // 0 + 70, 26, 6, 12, // 1 + 48, 52, 5, 12, // 2 + 66, 52, 5, 12, // 3 + 105, 26, 6, 12, // 4 + 72, 52, 5, 12, // 5 + 78, 52, 5, 12, // 6 + 84, 52, 5, 12, // 7 + 96, 52, 5, 12, // 8 + 102, 52, 5, 12, // 9 + 82, 65, 2, 12, // : + 67, 65, 2, 12, // ; + 120, 52, 5, 12, // < + 132, 52, 5, 12, // = + 105, 39, 5, 12, // > + 118, 13, 6, 12, // ? + 34, 0, 10, 12, // @ + 96, 13, 7, 12, // A + 125, 13, 6, 12, // B + 214, 0, 7, 12, // C + 132, 13, 6, 12, // D + 111, 39, 5, 12, // E + 123, 39, 5, 12, // F + 24, 13, 7, 12, // G + 146, 13, 6, 12, // H + 70, 65, 2, 12, // I + 141, 39, 5, 12, // J + 160, 13, 6, 12, // K + 167, 13, 6, 12, // L + 98, 0, 9, 12, // M + 174, 13, 6, 12, // N + 190, 0, 7, 12, // O + 181, 13, 6, 12, // P + 206, 0, 7, 12, // Q + 244, 13, 6, 12, // R + 0, 26, 6, 12, // S + 7, 26, 6, 12, // T + 28, 26, 6, 12, // U + 198, 0, 7, 12, // V + 118, 0, 9, 12, // W + 42, 26, 6, 12, // X + 56, 26, 6, 12, // Y + 216, 13, 6, 12, // Z + 16, 65, 3, 12, // [ + 4, 65, 3, 12, // Backslash + 244, 52, 3, 12, // ] + 159, 39, 5, 12, // ^ + 112, 26, 6, 12, // _ + 240, 52, 3, 12, // ` + 126, 26, 6, 12, // a + 133, 26, 6, 12, // b + 171, 39, 5, 12, // c + 231, 26, 6, 12, // d + 104, 13, 6, 12, // e + 199, 52, 4, 12, // f + 111, 13, 6, 12, // g + 139, 13, 6, 12, // h + 91, 65, 2, 12, // i + 88, 65, 2, 12, // j + 35, 26, 6, 12, // k + 64, 65, 2, 12, // l + 12, 0, 10, 12, // m + 119, 26, 6, 12, // n + 140, 26, 6, 12, // o + 77, 26, 6, 12, // p + 188, 13, 6, 12, // q + 174, 52, 4, 12, // r + 195, 13, 6, 12, // s + 251, 13, 4, 12, // t + 209, 13, 6, 12, // u + 219, 39, 5, 12, // v + 88, 0, 9, 12, // w + 231, 39, 5, 12, // x + 237, 39, 5, 12, // y + 243, 39, 5, 12, // z + 144, 52, 4, 12, // { + 254, 0, 1, 12, // | + 149, 52, 4, 12, // } + 12, 52, 5, 12, // ~ + 118, 13, 6, 12, // ? + 223, 13, 6, 12, // € + 118, 13, 6, 12, // ? + 58, 65, 2, 12, // ‚ + 154, 52, 4, 12, // ƒ + 42, 52, 5, 12, // „ + 56, 0, 10, 12, // … + 54, 52, 5, 12, // † + 60, 52, 5, 12, // ‡ + 169, 52, 4, 12, // ˆ + 23, 0, 10, 12, // ‰ + 202, 13, 6, 12, // Š + 28, 65, 3, 12, // ‹ + 45, 0, 10, 12, // Œ + 118, 13, 6, 12, // ? + 237, 13, 6, 12, // Ž + 118, 13, 6, 12, // ? + 118, 13, 6, 12, // ? + 61, 65, 2, 12, // ‘ + 40, 65, 2, 12, // ’ + 0, 52, 5, 12, // “ + 249, 39, 5, 12, // ” + 12, 65, 3, 12, // • + 230, 13, 6, 12, // – + 0, 0, 11, 12, // — + 179, 52, 4, 12, // ˜ + 78, 0, 9, 12, // ™ + 63, 26, 6, 12, // š + 36, 65, 3, 12, // › + 108, 0, 9, 12, // œ + 118, 13, 6, 12, // ? + 165, 39, 5, 12, // ž + 91, 26, 6, 12, // Ÿ + 248, 52, 3, 12, //   + 85, 65, 2, 12, // ¡ + 177, 39, 5, 12, // ¢ + 153, 39, 5, 12, // £ + 147, 39, 5, 12, // ¤ + 153, 13, 6, 12, // ¥ + 94, 65, 1, 12, // ¦ + 108, 52, 5, 12, // § + 20, 65, 3, 12, // ¨ + 182, 0, 7, 12, // © + 189, 52, 4, 12, // ª + 30, 52, 5, 12, // « + 24, 52, 5, 12, // ¬ + 204, 52, 3, 12, // ­ + 88, 13, 7, 12, // ® + 49, 26, 6, 12, // ¯ + 216, 52, 3, 12, // ° + 225, 39, 5, 12, // ± + 224, 52, 3, 12, // ² + 228, 52, 3, 12, // ³ + 232, 52, 3, 12, // ´ + 213, 39, 5, 12, // µ + 14, 26, 6, 12, // ¶ + 49, 65, 2, 12, // · + 52, 65, 2, 12, // ¸ + 252, 52, 3, 12, // ¹ + 0, 65, 3, 12, // º + 189, 39, 5, 12, // » + 156, 0, 8, 12, // ¼ + 222, 0, 7, 12, // ½ + 128, 0, 9, 12, // ¾ + 21, 26, 6, 12, // ¿ + 238, 0, 7, 12, // À + 246, 0, 7, 12, // Á + 0, 13, 7, 12, //  + 8, 13, 7, 12, // à + 16, 13, 7, 12, // Ä + 174, 0, 7, 12, // Å + 67, 0, 10, 12, // Æ + 32, 13, 7, 12, // Ç + 117, 39, 5, 12, // È + 114, 52, 5, 12, // É + 129, 39, 5, 12, // Ê + 135, 39, 5, 12, // Ë + 73, 65, 2, 12, // Ì + 76, 65, 2, 12, // Í + 159, 52, 4, 12, // Î + 164, 52, 4, 12, // Ï + 40, 13, 7, 12, // Ð + 98, 26, 6, 12, // Ñ + 48, 13, 7, 12, // Ò + 56, 13, 7, 12, // Ó + 64, 13, 7, 12, // Ô + 72, 13, 7, 12, // Õ + 80, 13, 7, 12, // Ö + 183, 39, 5, 12, // × + 165, 0, 8, 12, // Ø + 147, 26, 6, 12, // Ù + 154, 26, 6, 12, // Ú + 161, 26, 6, 12, // Û + 168, 26, 6, 12, // Ü + 175, 26, 6, 12, // Ý + 182, 26, 6, 12, // Þ + 189, 26, 6, 12, // ß + 196, 26, 6, 12, // à + 203, 26, 6, 12, // á + 210, 26, 6, 12, // â + 217, 26, 6, 12, // ã + 84, 26, 6, 12, // ä + 224, 26, 6, 12, // å + 138, 0, 8, 12, // æ + 18, 52, 5, 12, // ç + 238, 26, 6, 12, // è + 245, 26, 6, 12, // é + 0, 39, 6, 12, // ê + 7, 39, 6, 12, // ë + 208, 52, 3, 12, // ì + 212, 52, 3, 12, // í + 184, 52, 4, 12, // î + 32, 65, 3, 12, // ï + 14, 39, 6, 12, // ð + 21, 39, 6, 12, // ñ + 28, 39, 6, 12, // ò + 35, 39, 6, 12, // ó + 42, 39, 6, 12, // ô + 49, 39, 6, 12, // õ + 56, 39, 6, 12, // ö + 90, 52, 5, 12, // ÷ + 63, 39, 6, 12, // ø + 70, 39, 6, 12, // ù + 77, 39, 6, 12, // ú + 84, 39, 6, 12, // û + 91, 39, 6, 12, // ü + 126, 52, 5, 12, // ý + 98, 39, 6, 12, // þ + 138, 52, 5, 12, // ÿ + 118, 13, 6, 12, // ? +}; diff --git a/source/gui/gfx/fontSmall_0.grit b/source/gui/gfx/fontSmall_0.grit new file mode 100644 index 0000000..384c666 --- /dev/null +++ b/source/gui/gfx/fontSmall_0.grit @@ -0,0 +1 @@ +-gb -gB8 diff --git a/source/gui/gfx/fontSmall_0.png b/source/gui/gfx/fontSmall_0.png new file mode 100644 index 0000000..fa56622 Binary files /dev/null and b/source/gui/gfx/fontSmall_0.png differ diff --git a/source/gui/gfx/shiftKey.grit b/source/gui/gfx/shiftKey.grit new file mode 100644 index 0000000..499b4d2 --- /dev/null +++ b/source/gui/gfx/shiftKey.grit @@ -0,0 +1 @@ +-gb -gB8 -gTFF00FF diff --git a/source/gui/gfx/shiftKey.png b/source/gui/gfx/shiftKey.png new file mode 100644 index 0000000..76479e0 Binary files /dev/null and b/source/gui/gfx/shiftKey.png differ diff --git a/source/gui/image.c b/source/gui/image.c new file mode 100644 index 0000000..247476c --- /dev/null +++ b/source/gui/image.c @@ -0,0 +1,309 @@ +#include "image.h" + +#include +#include + +struct GuiImage { + size_t width; + size_t height; + size_t posX; + size_t posY; + int scale; + GuiImageHAlign hAlign; + GuiImageVAlign vAlign; + glImage* image; + int textureId; + int colorTintPalId; +}; + +void bitmapToRGB5A1(u8* bitmap, size_t width, size_t height, GuiImageTextureType textureType) +{ + size_t pixelCount = width * height; + u16* rgb5a1Bitmap = (u16*)bitmap; + + for (size_t i = 0; i < pixelCount; i++) { + u8 r = bitmap[i * (textureType == GUI_IMAGE_TEXTURE_TYPE_RGB ? 3 : 4)]; + u8 g = bitmap[i * (textureType == GUI_IMAGE_TEXTURE_TYPE_RGB ? 3 : 4) + 1]; + u8 b = bitmap[i * (textureType == GUI_IMAGE_TEXTURE_TYPE_RGB ? 3 : 4) + 2]; + u8 a = (textureType == GUI_IMAGE_TEXTURE_TYPE_RGB) ? 255 : bitmap[i * 4 + 3]; // Alpha is 255 if format is RGB + + u16 rgb5a1 = 0; + rgb5a1 |= (r >> 3) & 0x1F; // 5 bits for red + rgb5a1 |= ((g >> 3) & 0x1F) << 5; // 5 bits for green + rgb5a1 |= ((b >> 3) & 0x1F) << 10; // 5 bits for blue + rgb5a1 |= (a >> 7) << 15; // 1 bit for alpha (most significant bit of alpha) + + rgb5a1Bitmap[i] = rgb5a1; + } +} + +bool pngFileToBitmap(const char* filePath, u8** bitmap, size_t* width, size_t* height, GuiImageTextureType* textureType) +{ + FILE* fp = fopen(filePath, "rb"); + if (!fp) + return false; + + png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!png) { + fclose(fp); + return false; + } + + png_infop info = png_create_info_struct(png); + if (!info) { + png_destroy_read_struct(&png, NULL, NULL); + fclose(fp); + return false; + } + + if (setjmp(png_jmpbuf(png))) { + png_destroy_read_struct(&png, &info, NULL); + fclose(fp); + return false; + } + + png_init_io(png, fp); + png_read_info(png, info); + + *width = png_get_image_width(png, info); + *height = png_get_image_height(png, info); + + // 1024 is the max texture size supported + if (*width > 1024 || *height > 1024) { + png_destroy_read_struct(&png, &info, NULL); + fclose(fp); + return false; + } + + png_byte color_type = png_get_color_type(png, info); + png_byte bit_depth = png_get_bit_depth(png, info); + + // Convert to 8-bit depth if necessary + if (bit_depth == 16) + png_set_strip_16(png); + else if (bit_depth < 8) + png_set_packing(png); + + // Convert palette images to RGB + if (color_type == PNG_COLOR_TYPE_PALETTE) + png_set_palette_to_rgb(png); + + // Convert grayscale images to RGB + if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) + png_set_gray_to_rgb(png); + + // Add alpha channel if necessary + if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE) + png_set_filler(png, 0xFF, PNG_FILLER_AFTER); + + // Convert transparency to alpha + if (png_get_valid(png, info, PNG_INFO_tRNS)) + png_set_tRNS_to_alpha(png); + + png_read_update_info(png, info); + + *bitmap = (u8*)malloc(png_get_rowbytes(png, info) * (*height)); + png_bytep* row_pointers = (png_bytep*)malloc(sizeof(png_bytep) * (*height)); + for (size_t y = 0; y < *height; y++) + row_pointers[y] = *bitmap + y * png_get_rowbytes(png, info); + + png_read_image(png, row_pointers); + + *textureType = GUI_IMAGE_TEXTURE_TYPE_RGBA; + + bitmapToRGB5A1(*bitmap, *width, *height, *textureType); + + fclose(fp); + png_destroy_read_struct(&png, &info, NULL); + free(row_pointers); + + return true; +} + +// Calculate closest GL_TEXTURE_SIZE_ENUM +#define calculateGlTextureSizeEnum(x) \ + ((x) <= 8 ? TEXTURE_SIZE_8 \ + : (x) <= 16 ? TEXTURE_SIZE_16 \ + : (x) <= 32 ? TEXTURE_SIZE_32 \ + : (x) <= 64 ? TEXTURE_SIZE_64 \ + : (x) <= 128 ? TEXTURE_SIZE_128 \ + : (x) <= 256 ? TEXTURE_SIZE_256 \ + : (x) <= 512 ? TEXTURE_SIZE_512 \ + : (x) <= 1024 ? TEXTURE_SIZE_1024 \ + : 0) + +GuiImage newGuiImage(const unsigned* bitmap, const u16* pal, size_t width, size_t height, size_t bitmapWidth, size_t bitmapHeight, size_t resizeWidth, size_t resizeHeight, GuiImageTextureType textureType) +{ + GuiImage gi = malloc(sizeof(struct GuiImage)); + gi->width = width; + gi->height = height; + gi->scale = 1 << 12; + gi->hAlign = GUI_IMAGE_H_ALIGN_LEFT; + gi->vAlign = GUI_IMAGE_V_ALIGN_TOP; + gi->posX = 0; + gi->posY = 0; + gi->image = malloc(sizeof(glImage)); + gi->colorTintPalId = 0; + + size_t glTextureSizeWidth = calculateGlTextureSizeEnum(bitmapWidth); + size_t glTextureSizeHeight = calculateGlTextureSizeEnum(bitmapHeight); + + GL_TEXTURE_TYPE_ENUM glTextureType; + switch (textureType) { + case GUI_IMAGE_TEXTURE_TYPE_RGB: + glTextureType = GL_RGB; + break; + case GUI_IMAGE_TEXTURE_TYPE_RGBA: + glTextureType = GL_RGBA; + break; + default: + glTextureType = GL_RGB256; + break; + } + + gi->textureId = glLoadTileSet( + gi->image, + width, + height, + bitmapWidth, + bitmapHeight, + glTextureType, + glTextureSizeWidth, + glTextureSizeHeight, + TEXGEN_OFF | GL_TEXTURE_COLOR0_TRANSPARENT, + (textureType == GUI_IMAGE_TEXTURE_TYPE_RGB256 ? 256 : 0), + pal, + (u8*)bitmap); + + // Set scale to fit inside resized width and height + if (resizeWidth || resizeHeight) { + double scaleFactor = 1.0; + + if (resizeWidth && !resizeHeight) { + scaleFactor = (double)resizeWidth / width; + gi->width = resizeWidth; + gi->height *= scaleFactor; + } else if (!resizeWidth && resizeHeight) { + scaleFactor = (double)resizeHeight / height; + gi->height = resizeHeight; + gi->width *= scaleFactor; + } else { + if (resizeWidth <= resizeHeight) { + scaleFactor = (double)resizeWidth / width; + gi->width = resizeWidth; + gi->height *= scaleFactor; + } else { + scaleFactor = (double)resizeHeight / height; + gi->height = resizeHeight; + gi->width *= scaleFactor; + } + } + + gi->scale *= scaleFactor; + } + + return gi; +} + +GuiImage newGuiImagePNG(const char* filePath, size_t resizeWidth, size_t resizeHeight) +{ + unsigned* bitmap = NULL; + GuiImageTextureType textureType; + size_t width, height; + + if (!pngFileToBitmap(filePath, (u8**)&bitmap, &width, &height, &textureType)) + return NULL; + + GuiImage gi = newGuiImage(bitmap, NULL, width, height, width, height, resizeWidth, resizeHeight, textureType); + free(bitmap); + + return gi; +} + +void freeGuiImage(GuiImage gi) +{ + glDeleteTextures(1, &gi->textureId); + + if (gi->colorTintPalId) + glDeleteTextures(1, &gi->colorTintPalId); + + free(gi->image); + free(gi); +} + +void setGuiImagePos(GuiImage gi, size_t posX, size_t posY) +{ + gi->posX = posX; + gi->posY = posY; +} + +void setGuiImageAlign(GuiImage gi, GuiImageHAlign hAlignment, GuiImageVAlign vAlignment) +{ + gi->hAlign = hAlignment; + gi->vAlign = vAlignment; +} + +void setGuiImageColorTint(GuiImage gi, u16 color) +{ + u16 colorTintPal[256]; + for (u16 i = 0; i < 256; i++) + colorTintPal[i] = color; + + glGenTextures(1, &gi->colorTintPalId); + glBindTexture(0, gi->colorTintPalId); + glColorTableEXT(0, 0, 256, 0, 0, colorTintPal); +} + +size_t getGuiImageWidth(GuiImage gi) +{ + return gi->width; +} + +size_t getGuiImageHeight(GuiImage gi) +{ + return gi->height; +} + +void drawGuiImagePos(GuiImage gi, size_t posX, size_t posY) +{ + size_t x; + size_t y; + + switch (gi->hAlign) { + case GUI_IMAGE_H_ALIGN_CENTER: + x = posX - gi->width / 2; + break; + case GUI_IMAGE_H_ALIGN_RIGHT: + x = posX - gi->width; + break; + default: + x = posX; + break; + } + + switch (gi->vAlign) { + case GUI_IMAGE_V_ALIGN_MIDDLE: + y = posY - gi->height / 2; + break; + case GUI_IMAGE_V_ALIGN_BOTTOM: + y = posY + gi->height; + break; + default: + y = posY; + break; + } + + glSetActiveTexture(gi->textureId); + + if (gi->colorTintPalId) + glAssignColorTable(0, gi->colorTintPalId); + + glPolyFmt(POLY_ALPHA(31) | POLY_CULL_NONE | POLY_ID(1)); + + glSpriteScale(x, y, gi->scale, GL_FLIP_NONE, gi->image); +} + +void drawGuiImage(GuiImage gi) +{ + drawGuiImagePos(gi, gi->posX, gi->posY); +} diff --git a/source/gui/image.h b/source/gui/image.h new file mode 100644 index 0000000..2ae8dbc --- /dev/null +++ b/source/gui/image.h @@ -0,0 +1,33 @@ +#pragma once +#include + +typedef struct GuiImage* GuiImage; + +typedef enum { + GUI_IMAGE_TEXTURE_TYPE_RGB, + GUI_IMAGE_TEXTURE_TYPE_RGBA, + GUI_IMAGE_TEXTURE_TYPE_RGB256 +} GuiImageTextureType; + +typedef enum { + GUI_IMAGE_H_ALIGN_LEFT, + GUI_IMAGE_H_ALIGN_CENTER, + GUI_IMAGE_H_ALIGN_RIGHT +} GuiImageHAlign; + +typedef enum { + GUI_IMAGE_V_ALIGN_TOP, + GUI_IMAGE_V_ALIGN_MIDDLE, + GUI_IMAGE_V_ALIGN_BOTTOM +} GuiImageVAlign; + +GuiImage newGuiImage(const unsigned* bitmap, const u16* pal, size_t width, size_t height, size_t bitmapWidth, size_t bitmapHeight, size_t resizeWidth, size_t resizeHeight, GuiImageTextureType textureType); +GuiImage newGuiImagePNG(const char* filePath, size_t resizeWidth, size_t resizeHeight); +void freeGuiImage(GuiImage); +void setGuiImagePos(GuiImage, size_t posX, size_t posY); +void setGuiImageAlign(GuiImage, GuiImageHAlign, GuiImageVAlign); +void setGuiImageColorTint(GuiImage, u16 color); +size_t getGuiImageWidth(GuiImage); +size_t getGuiImageHeight(GuiImage); +void drawGuiImagePos(GuiImage, size_t posX, size_t posY); +void drawGuiImage(GuiImage); diff --git a/source/gui/input.c b/source/gui/input.c new file mode 100644 index 0000000..0deb434 --- /dev/null +++ b/source/gui/input.c @@ -0,0 +1,12 @@ +#include "input.h" + +u32 pressed; +touchPosition touch; + +void updateInput(void) +{ + scanKeys(); + pressed = keysDown(); + + touchRead(&touch); +} diff --git a/source/gui/input.h b/source/gui/input.h new file mode 100644 index 0000000..e580c65 --- /dev/null +++ b/source/gui/input.h @@ -0,0 +1,7 @@ +#pragma once +#include + +extern u32 pressed; +extern touchPosition touch; + +void updateInput(void); diff --git a/source/gui/keyboard.c b/source/gui/keyboard.c new file mode 100644 index 0000000..b75f23f --- /dev/null +++ b/source/gui/keyboard.c @@ -0,0 +1,156 @@ +#include "keyboard.h" + +#include "backspaceKey.h" +#include "shiftKey.h" +#include + +struct GuiKeyboard { + size_t posX; + size_t posY; + struct GuiKeyboardKey* keys; + bool shiftPressed; +}; + +#define KEYS_COUNT 43 + +void setKeys(GuiKeyboard gk) +{ + const char* chars; + + if (!gk->shiftPressed) + chars = "1234567890\0qwertyuiop@asdfghjkl\0zxcvbnm-. \0"; + else + chars = "1234567890\0QWERTYUIOP+ASDFGHJKL\0ZXCVBNM_/ \0"; + + for (size_t i = 0; i < KEYS_COUNT; i++) { + gk->keys[i].c = chars[i]; + gk->keys[i].extraKey = GUI_KEYBOARD_EXTRA_KEY_NONE; + + char text[2] = { chars[i], '\0' }; + setGuiTextText(gk->keys[i].label, text); + } + + gk->keys[10].extraKey = GUI_KEYBOARD_EXTRA_KEY_BACKSPACE; + gk->keys[31].extraKey = GUI_KEYBOARD_EXTRA_KEY_SHIFT; +} + +void setKeysPos(GuiKeyboard gk) +{ + // First row + for (size_t i = 0; i < 11; i++) + setGuiButtonPos(gk->keys[i].btn, gk->posX + i * 23, gk->posY); + + // Second row + for (size_t i = 11; i < 22; i++) + setGuiButtonPos(gk->keys[i].btn, gk->posX + (i - 11) * 23 + 1, gk->posY + 23); + + // Third row + for (size_t i = 22; i < 32; i++) + setGuiButtonPos(gk->keys[i].btn, gk->posX + (i - 22) * 23 + 11, gk->posY + 23 * 2); + + // Fourth row + for (size_t i = 32; i < 41; i++) + setGuiButtonPos(gk->keys[i].btn, gk->posX + (i - 32) * 23 + 22, gk->posY + 23 * 3); + + // Space + setGuiButtonPos(gk->keys[41].btn, gk->posX + 70, gk->posY + 23 * 4); +} + +void setKeysShift(GuiKeyboard gk) +{ + gk->shiftPressed = (gk->shiftPressed ? false : true); + setKeys(gk); +} + +GuiKeyboard newGuiKeyboard(u16 textColor, u16 hoverColor) +{ + GuiKeyboard gk = malloc(sizeof(struct GuiKeyboard)); + gk->posX = 0; + gk->posY = 0; + gk->keys = malloc(sizeof(struct GuiKeyboardKey) * KEYS_COUNT); + gk->shiftPressed = false; + + // All keys + for (size_t i = 0; i < KEYS_COUNT; i++) { + gk->keys[i].bg = newGuiBox(23, 23, 0); + + gk->keys[i].bgHover = newGuiBox(23, 23, 0); + setGuiBoxBorder(gk->keys[i].bgHover, 1, hoverColor); + + gk->keys[i].label = newGuiText("", GUI_TEXT_SIZE_MEDIUM, textColor); + setGuiTextAlignment(gk->keys[i].label, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + gk->keys[i].btn = newGuiButton(23, 23); + setGuiButtonBg(gk->keys[i].btn, gk->keys[i].bg, gk->keys[i].bgHover); + setGuiButtonLabel(gk->keys[i].btn, gk->keys[i].label); + } + + // Space + setGuiBoxWidth(gk->keys[41].bg, 115); + setGuiBoxBorder(gk->keys[41].bg, 1, textColor); + setGuiBoxWidth(gk->keys[41].bgHover, 115); + setGuiButtonWidth(gk->keys[41].btn, 115); + + // Backspace + gk->keys[10].icon = newGuiImage(backspaceKeyBitmap, backspaceKeyPal, 15, 11, 16, 16, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + setGuiImageColorTint(gk->keys[10].icon, textColor); + setGuiButtonIcon(gk->keys[10].btn, gk->keys[10].icon, gk->keys[10].icon); + + // Shift + gk->keys[31].icon = newGuiImage(shiftKeyBitmap, shiftKeyPal, 11, 13, 16, 16, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + setGuiImageColorTint(gk->keys[31].icon, textColor); + setGuiButtonIcon(gk->keys[31].btn, gk->keys[31].icon, gk->keys[31].icon); + + setKeysPos(gk); + + setKeys(gk); + + return gk; +} + +void freeGuiKeyboard(GuiKeyboard gk) +{ + for (size_t i = 0; i < KEYS_COUNT; i++) { + freeGuiBox(gk->keys[i].bg); + freeGuiBox(gk->keys[i].bgHover); + freeGuiText(gk->keys[i].label); + freeGuiButton(gk->keys[i].btn); + } + + freeGuiImage(gk->keys[10].icon); // Backspace + freeGuiImage(gk->keys[31].icon); // Shift + + free(gk->keys); + free(gk); +} + +void setGuiKeyboardPos(GuiKeyboard gk, size_t posX, size_t posY) +{ + gk->posX = posX; + gk->posY = posY; + setKeysPos(gk); +} + +struct GuiKeyboardKey getGuiKeyboardPressed(GuiKeyboard gk) +{ + for (size_t i = 0; i < KEYS_COUNT; i++) { + if (getGuiButtonState(gk->keys[i].btn) != GUI_BUTTON_STATE_CLICKED) + continue; + + if (i == 31) // Shift + setKeysShift(gk); + + resetGuiButtonState(gk->keys[i].btn); + return gk->keys[i]; + } + + return gk->keys[KEYS_COUNT - 1]; // None key +} + +void drawGuiKeyboard(GuiKeyboard gk) +{ + for (size_t i = 0; i < KEYS_COUNT; i++) { + handleTouchGuiButton(gk->keys[i].btn); + drawGuiButton(gk->keys[i].btn); + } +} diff --git a/source/gui/keyboard.h b/source/gui/keyboard.h new file mode 100644 index 0000000..85dfef9 --- /dev/null +++ b/source/gui/keyboard.h @@ -0,0 +1,26 @@ +#pragma once +#include "button.h" + +typedef struct GuiKeyboard* GuiKeyboard; + +typedef enum { + GUI_KEYBOARD_EXTRA_KEY_NONE, + GUI_KEYBOARD_EXTRA_KEY_BACKSPACE, + GUI_KEYBOARD_EXTRA_KEY_SHIFT +} GuiKeyboardExtraKey; + +struct GuiKeyboardKey { + GuiBox bg; + GuiBox bgHover; + GuiText label; + GuiButton btn; + GuiImage icon; + char c; + GuiKeyboardExtraKey extraKey; +}; + +GuiKeyboard newGuiKeyboard(u16 textColor, u16 hoverColor); +void freeGuiKeyboard(GuiKeyboard); +void setGuiKeyboardPos(GuiKeyboard, size_t posX, size_t posY); +struct GuiKeyboardKey getGuiKeyboardPressed(GuiKeyboard); +void drawGuiKeyboard(GuiKeyboard); diff --git a/source/gui/progressbar.c b/source/gui/progressbar.c new file mode 100644 index 0000000..c35b5ba --- /dev/null +++ b/source/gui/progressbar.c @@ -0,0 +1,67 @@ +#include "progressbar.h" + +#include + +struct GuiProgressBar { + size_t width; + size_t height; + size_t posX; + size_t posY; + u16 bgColor; + u16 progressColor; + u8 percent; +}; + +GuiProgressbar newGuiProgressbar(size_t width, size_t height, u16 bgColor, u16 progressColor) +{ + GuiProgressbar gp = malloc(sizeof(struct GuiProgressBar)); + gp->width = width; + gp->height = height; + gp->posX = 0; + gp->posY = 0; + gp->bgColor = bgColor; + gp->progressColor = progressColor; + gp->percent = 0; + + return gp; +} + +void freeGuiProgressbar(GuiProgressbar gp) +{ + free(gp); +} + +void setGuiProgressbarPos(GuiProgressbar gp, size_t posX, size_t posY) +{ + gp->posX = posX; + gp->posY = posY; +} + +void setGuiProgressbarPercent(GuiProgressbar gp, u8 percent) +{ + gp->percent = percent; +} + +void drawGuiProgressbarPos(GuiProgressbar gp, size_t posX, size_t posY) +{ + glBoxFilled( + posX, + posY, + posX + gp->width - 1, + posY + gp->height - 1, + gp->bgColor); + + if (gp->percent) { + glBoxFilled( + posX, + posY, + posX + (gp->width * gp->percent / 100) - 1, + posY + gp->height - 1, + gp->progressColor); + } +} + +void drawGuiProgressbar(GuiProgressbar gp) +{ + drawGuiProgressbarPos(gp, gp->posX, gp->posY); +} diff --git a/source/gui/progressbar.h b/source/gui/progressbar.h new file mode 100644 index 0000000..39e0f25 --- /dev/null +++ b/source/gui/progressbar.h @@ -0,0 +1,11 @@ +#pragma once +#include + +typedef struct GuiProgressBar* GuiProgressbar; + +GuiProgressbar newGuiProgressbar(size_t width, size_t height, u16 bgColor, u16 progressColor); +void freeGuiProgressbar(GuiProgressbar); +void setGuiProgressbarPos(GuiProgressbar, size_t posX, size_t posY); +void setGuiProgressbarPercent(GuiProgressbar, u8); +void drawGuiProgressbarPos(GuiProgressbar, size_t posX, size_t posY); +void drawGuiProgressbar(GuiProgressbar); diff --git a/source/gui/screen.c b/source/gui/screen.c new file mode 100644 index 0000000..a795a63 --- /dev/null +++ b/source/gui/screen.c @@ -0,0 +1,318 @@ +#include "screen.h" + +#include "button.h" +#include "input.h" +#include "keyboard.h" +#include "progressbar.h" +#include +#include + +struct ElementNode { + void* value; + GuiElementType type; + struct ElementNode* next; +}; + +struct ElementList { + struct ElementNode* head; + struct ElementNode* tail; + struct ElementNode* selected; + size_t size; +}; + +struct GuiScreen { + GuiScreenLcd lcd; + bool dpadNavigate; + struct ElementList* elements; +}; + +GuiScreen activeTopScreen = NULL; +GuiScreen activeBottomScreen = NULL; + +GuiScreenLcd targetLcd = GUI_SCREEN_LCD_TOP; + +GuiScreen newGuiScreen(GuiScreenLcd lcd) +{ + GuiScreen gs = malloc(sizeof(struct GuiScreen)); + gs->lcd = lcd; + gs->dpadNavigate = true; + + struct ElementList* el = malloc(sizeof(struct ElementList)); + el->head = NULL; + el->tail = NULL; + el->selected = NULL; + el->size = 0; + gs->elements = el; + + return gs; +} + +void freeGuiScreen(GuiScreen gs) +{ + struct ElementNode* curr = gs->elements->head; + while (curr != NULL) { + struct ElementNode* next = curr->next; + free(curr); + curr = next; + } + + free(gs->elements); + free(gs); +} + +void setGuiScreenDpadNavigate(GuiScreen gs, bool dpadNavigate) +{ + gs->dpadNavigate = dpadNavigate; +} + +void addToGuiScreen(GuiScreen gs, void* element, GuiElementType type) +{ + struct ElementNode* en = malloc(sizeof(struct ElementNode)); + en->value = element; + en->type = type; + en->next = NULL; + + if (gs->elements->head == NULL) { + gs->elements->head = en; + gs->elements->tail = en; + } else { + gs->elements->tail->next = en; + gs->elements->tail = en; + } + + gs->elements->size++; +} + +void removeFromGuiScreen(GuiScreen gs, void* element) +{ + struct ElementNode* prev = NULL; + struct ElementNode* curr = gs->elements->head; + + while (curr != NULL) { + if (curr->value == element) { + if (prev == NULL) + gs->elements->head = curr->next; + else + prev->next = curr->next; + + if (curr == gs->elements->tail) + gs->elements->tail = prev; + + free(curr); + gs->elements->size--; + break; + } + + prev = curr; + curr = curr->next; + } +} + +void drawGuiScreen(GuiScreen gs) +{ + glBegin2D(); + + struct ElementNode* curr; + curr = gs->elements->head; + while (curr != NULL) { + switch (curr->type) { + case GUI_ELEMENT_TYPE_BOX: + drawGuiBox(curr->value); + break; + case GUI_ELEMENT_TYPE_IMAGE: + drawGuiImage(curr->value); + break; + case GUI_ELEMENT_TYPE_BUTTON: + if (gs->lcd == GUI_SCREEN_LCD_BOTTOM) + handleTouchGuiButton(curr->value); + + drawGuiButton(curr->value); + break; + case GUI_ELEMENT_TYPE_TEXT: + drawGuiText(curr->value); + break; + case GUI_ELEMENT_TYPE_PROGRESSBAR: + drawGuiProgressbar(curr->value); + break; + case GUI_ELEMENT_TYPE_KEYBOARD: + drawGuiKeyboard(curr->value); + break; + } + + curr = curr->next; + } + + glEnd2D(); +} + +void setActiveScreens(GuiScreen topScreen, GuiScreen bottomScreen) +{ + activeTopScreen = topScreen; + activeBottomScreen = bottomScreen; +} + +GuiScreen getActiveTopScreen(void) +{ + return activeTopScreen; +} + +GuiScreen getActiveBottomScreen(void) +{ + return activeBottomScreen; +} + +void handleScreensDpadNavigate(void) +{ + if ((!activeTopScreen || !activeTopScreen->dpadNavigate) && (!activeBottomScreen || !activeBottomScreen->dpadNavigate)) + return; + + if (!(pressed & (KEY_DOWN | KEY_UP | KEY_LEFT | KEY_RIGHT | KEY_A))) + return; + + GuiButton selected = NULL; + size_t topScreenSize = activeTopScreen ? activeTopScreen->elements->size : 0; + size_t bottomScreenSize = activeBottomScreen ? activeBottomScreen->elements->size : 0; + GuiButton buttons[topScreenSize + bottomScreenSize]; + size_t buttonsCount = 0; + + struct ElementNode* curr; + + // Collect buttons from the top screen + if (activeTopScreen && activeTopScreen->dpadNavigate) { + curr = activeTopScreen->elements->head; + for (; curr != NULL; curr = curr->next) { + if (curr->type != GUI_ELEMENT_TYPE_BUTTON) + continue; + + if (getGuiButtonState(curr->value) == GUI_BUTTON_STATE_SELECTED) + selected = curr->value; + + buttons[buttonsCount++] = curr->value; + } + } + + // Collect buttons from the bottom screen + if (activeBottomScreen && activeBottomScreen->dpadNavigate) { + curr = activeBottomScreen->elements->head; + for (; curr != NULL; curr = curr->next) { + if (curr->type != GUI_ELEMENT_TYPE_BUTTON) + continue; + + if (getGuiButtonState(curr->value) == GUI_BUTTON_STATE_SELECTED) + selected = curr->value; + + buttons[buttonsCount++] = curr->value; + } + } + + if (buttonsCount == 0) + return; + + // Find the highest button and select it if no button is selected + if (!selected) { + selected = buttons[0]; + for (size_t i = 1; i < buttonsCount; i++) { + size_t selectedPosY = getGuiButtonPosY(selected) + (i < topScreenSize ? 192 : 0); + size_t buttonPosY = getGuiButtonPosY(buttons[i]) + (i < topScreenSize ? 192 : 0); + if (buttonPosY < selectedPosY) + selected = buttons[i]; + } + + setGuiButtonState(selected, GUI_BUTTON_STATE_SELECTED); + return; + } + + // Handle button click + if (pressed & KEY_A) { + if (selected) + setGuiButtonState(selected, GUI_BUTTON_STATE_CLICKED); + + return; + } + + // Find the closest button according to the pressed key. Account for both screens + GuiButton closest = NULL; + for (size_t i = 0; i < buttonsCount; i++) { + size_t selectedPosY = getGuiButtonPosY(selected) + (i < topScreenSize ? 192 : 0); + size_t buttonPosY = getGuiButtonPosY(buttons[i]) + (i < topScreenSize ? 192 : 0); + size_t selectedPosX = getGuiButtonPosX(selected); + size_t buttonPosX = getGuiButtonPosX(buttons[i]); + + if (pressed & KEY_DOWN) { + if (buttonPosY <= selectedPosY) + continue; + + if (!closest || buttonPosY < getGuiButtonPosY(closest) + (i < topScreenSize ? 192 : 0)) + closest = buttons[i]; + } else if (pressed & KEY_UP) { + if (buttonPosY >= selectedPosY) + continue; + + if (!closest || buttonPosY > getGuiButtonPosY(closest) + (i < topScreenSize ? 192 : 0)) + closest = buttons[i]; + } else if (pressed & KEY_LEFT) { + if (buttonPosX >= selectedPosX) + continue; + + if (!closest || buttonPosX > getGuiButtonPosX(closest)) + closest = buttons[i]; + } else if (pressed & KEY_RIGHT) { + if (buttonPosX <= selectedPosX) + continue; + + if (!closest || buttonPosX < getGuiButtonPosX(closest)) + closest = buttons[i]; + } + } + + // Update button states + if (closest) { + setGuiButtonState(selected, GUI_BUTTON_STATE_DEFAULT); + setGuiButtonState(closest, GUI_BUTTON_STATE_SELECTED); + } +} + +void drawScreens(void) +{ + updateInput(); + + handleScreensDpadNavigate(); + + // Wait for capture unit to be ready + while (REG_DISPCAPCNT & DCAP_ENABLE) { }; + + if (activeTopScreen && activeBottomScreen) { + if (targetLcd == GUI_SCREEN_LCD_TOP) { + lcdMainOnBottom(); + vramSetBankC(VRAM_C_LCD); + vramSetBankD(VRAM_D_SUB_SPRITE); + REG_DISPCAPCNT = DCAP_BANK(2) | DCAP_ENABLE | DCAP_SIZE(3); + + drawGuiScreen(activeTopScreen); + } else { + lcdMainOnTop(); + vramSetBankD(VRAM_D_LCD); + vramSetBankC(VRAM_C_SUB_BG); + REG_DISPCAPCNT = DCAP_BANK(3) | DCAP_ENABLE | DCAP_SIZE(3); + + drawGuiScreen(activeBottomScreen); + } + + // Swap target lcd + targetLcd = targetLcd == GUI_SCREEN_LCD_TOP ? GUI_SCREEN_LCD_BOTTOM : GUI_SCREEN_LCD_TOP; + } else if (activeTopScreen) { + lcdMainOnTop(); + vramSetBankD(VRAM_D_LCD); + vramSetBankC(VRAM_C_SUB_BG); + REG_DISPCAPCNT = DCAP_BANK(3) | DCAP_ENABLE | DCAP_SIZE(3); + + drawGuiScreen(activeTopScreen); + } else { + lcdMainOnBottom(); + vramSetBankC(VRAM_C_LCD); + vramSetBankD(VRAM_D_SUB_SPRITE); + REG_DISPCAPCNT = DCAP_BANK(2) | DCAP_ENABLE | DCAP_SIZE(3); + + drawGuiScreen(activeBottomScreen); + } +} diff --git a/source/gui/screen.h b/source/gui/screen.h new file mode 100644 index 0000000..fbe2a2f --- /dev/null +++ b/source/gui/screen.h @@ -0,0 +1,28 @@ +#pragma once +#include + +typedef struct GuiScreen* GuiScreen; + +typedef enum { + GUI_SCREEN_LCD_TOP, + GUI_SCREEN_LCD_BOTTOM +} GuiScreenLcd; + +typedef enum { + GUI_ELEMENT_TYPE_BOX, + GUI_ELEMENT_TYPE_IMAGE, + GUI_ELEMENT_TYPE_BUTTON, + GUI_ELEMENT_TYPE_TEXT, + GUI_ELEMENT_TYPE_PROGRESSBAR, + GUI_ELEMENT_TYPE_KEYBOARD +} GuiElementType; + +GuiScreen newGuiScreen(GuiScreenLcd); +void freeGuiScreen(GuiScreen); +void addToGuiScreen(GuiScreen, void* element, GuiElementType); +void removeFromGuiScreen(GuiScreen, void* element); +void setGuiScreenDpadNavigate(GuiScreen, bool); +void setActiveScreens(GuiScreen topScreen, GuiScreen bottomScreen); +GuiScreen getActiveTopScreen(void); +GuiScreen getActiveBottomScreen(void); +void drawScreens(void); diff --git a/source/gui/text.c b/source/gui/text.c new file mode 100644 index 0000000..2fc2e27 --- /dev/null +++ b/source/gui/text.c @@ -0,0 +1,377 @@ +#include "text.h" + +#include "fontBig_0.h" +#include "fontMedium_0.h" +#include "fontSmall_0.h" +#include "gfx/fontBigUVCoords.h" +#include "gfx/fontMediumUVCoords.h" +#include "gfx/fontSmallUVCoords.h" +#include +#include + +glImage fontBig[FONTBIG_NUM_IMAGES]; +glImage fontMedium[FONTBIG_NUM_IMAGES]; +glImage fontSmall[FONTSMALL_NUM_IMAGES]; + +int fontBigTextureId; +int fontMediumTextureId; +int fontSmallTextureId; + +struct GuiText { + size_t width; + size_t height; + size_t posX; + size_t posY; + char* text; + u16 color; + size_t length; + GuiTextHAlign hAlign; + GuiTextVAlign vAlign; + size_t maxWidth; + size_t maxHeight; + bool wrap; + int textureId; + glImage* font; +}; + +void initGuiFont(void) +{ + fontBigTextureId = glLoadSpriteSet( + fontBig, + FONTBIG_NUM_IMAGES, + fontBigTexCoords, + GL_RGB256, + TEXTURE_SIZE_256, + TEXTURE_SIZE_256, + TEXGEN_OFF | GL_TEXTURE_COLOR0_TRANSPARENT, + 256, + fontBig_0Pal, + (u8*)fontBig_0Bitmap); + + fontMediumTextureId = glLoadSpriteSet( + fontMedium, + FONTMEDIUM_NUM_IMAGES, + fontMediumTexCoords, + GL_RGB256, + TEXTURE_SIZE_256, + TEXTURE_SIZE_256, + TEXGEN_OFF | GL_TEXTURE_COLOR0_TRANSPARENT, + 256, + fontMedium_0Pal, + (u8*)fontMedium_0Bitmap); + + fontSmallTextureId = glLoadSpriteSet( + fontSmall, + FONTSMALL_NUM_IMAGES, + fontSmallTexCoords, + GL_RGB256, + TEXTURE_SIZE_256, + TEXTURE_SIZE_256, + TEXGEN_OFF | GL_TEXTURE_COLOR0_TRANSPARENT, + 256, + fontSmall_0Pal, + (u8*)fontSmall_0Bitmap); +} + +GuiText newGuiText(const char* text, GuiTextSize size, u16 color) +{ + GuiText gt = malloc(sizeof(struct GuiText)); + gt->width = 0; + gt->height = 0; + gt->posX = 0; + gt->posY = 0; + gt->text = NULL; + gt->color = color; + gt->hAlign = GUI_TEXT_H_ALIGN_LEFT; + gt->vAlign = GUI_TEXT_V_ALIGN_TOP; + gt->maxWidth = 0; + gt->maxHeight = 0; + gt->wrap = false; + + switch (size) { + case GUI_TEXT_SIZE_BIG: + gt->textureId = fontBigTextureId; + gt->font = fontBig; + break; + case GUI_TEXT_SIZE_MEDIUM: + gt->textureId = fontMediumTextureId; + gt->font = fontMedium; + break; + case GUI_TEXT_SIZE_SMALL: + gt->textureId = fontSmallTextureId; + gt->font = fontSmall; + break; + } + + setGuiTextText(gt, text); + + return gt; +} + +void freeGuiText(GuiText gt) +{ + free(gt->text); + free(gt); +} + +void updateWH(GuiText gt) +{ + size_t x = 0; + size_t y = 0; + + size_t lineWidth = 0; + size_t lineStart = 0; + size_t lastSpace = -1; + + for (size_t i = 0; i < gt->length; i++) { + glImage currChar = gt->font[gt->text[i] - 1]; + lineWidth += currChar.width + 1; + + if (gt->text[i] == ' ') + lastSpace = i; + + if (gt->maxWidth && (lineWidth > gt->maxWidth || gt->text[i] == '\n')) { + if (lineWidth > gt->maxWidth) { + if (lastSpace != -1 && lastSpace > lineStart) { + i = lastSpace; + lineWidth = 0; + + for (size_t j = lineStart; j <= lastSpace; j++) + lineWidth += gt->font[gt->text[j] - 1].width + 1; + + } else { + lineWidth -= currChar.width + 1; + i--; + } + } + + switch (gt->hAlign) { + case GUI_TEXT_H_ALIGN_LEFT: + x = 0; + break; + case GUI_TEXT_H_ALIGN_CENTER: + x = 0 - lineWidth / 2; + break; + case GUI_TEXT_H_ALIGN_RIGHT: + x = 0 - lineWidth; + break; + } + + for (size_t j = lineStart; j <= i; j++) { + currChar = gt->font[gt->text[j] - 1]; + + if (gt->text[j] != '\n') + x += currChar.width + 1; + } + + lineStart = i + 1; + lineWidth = 0; + y += currChar.height + 1; + lastSpace = -1; + + if (!gt->wrap || (gt->maxHeight && y + currChar.height > gt->maxHeight)) { + gt->width = x; + gt->height = y; + return; + } + } + } + + if (lineStart < gt->length) { + switch (gt->hAlign) { + case GUI_TEXT_H_ALIGN_LEFT: + x = 0; + break; + case GUI_TEXT_H_ALIGN_CENTER: + x = 0 - lineWidth / 2; + break; + case GUI_TEXT_H_ALIGN_RIGHT: + x = 0 - lineWidth; + break; + } + + for (size_t j = lineStart; j < gt->length; j++) { + glImage currChar = gt->font[gt->text[j] - 1]; + x += currChar.width + 1; + } + } + + gt->width = x; + gt->height = y + gt->font[0].height + 1; +} + +void setGuiTextPos(GuiText gt, size_t posX, size_t posY) +{ + gt->posX = posX; + gt->posY = posY; +} + +void setGuiTextText(GuiText gt, const char* text) +{ + free(gt->text); + gt->text = strdup(text); + gt->length = strlen(text); + updateWH(gt); +} + +void setGuiTextAlignment(GuiText gt, GuiTextHAlign hAlignment, GuiTextVAlign vAlignment) +{ + gt->hAlign = hAlignment; + gt->vAlign = vAlignment; +} + +void setGuiTextMaxWidth(GuiText gt, size_t maxWidth) +{ + gt->maxWidth = maxWidth; + updateWH(gt); +} + +void setGuiTextMaxHeight(GuiText gt, size_t maxHeight) +{ + gt->maxHeight = maxHeight; + updateWH(gt); +} + +void setGuiTextWrap(GuiText gt, bool wrap) +{ + gt->wrap = wrap; + updateWH(gt); +} + +size_t getGuiTextWidth(GuiText gt) +{ + return gt->width; +} + +size_t getGuiTextHeight(GuiText gt) +{ + return gt->height; +} + +size_t getGuiTextPosX(GuiText gt) +{ + return gt->posX; +} + +size_t getGuiTextPosY(GuiText gt) +{ + return gt->posY; +} + +GuiTextHAlign getGuiTextHAlignment(GuiText gt) +{ + return gt->hAlign; +} + +GuiTextVAlign getGuiTextVAlignment(GuiText gt) +{ + return gt->vAlign; +} + +void drawGuiTextPos(GuiText gt, size_t posX, size_t posY) +{ + size_t x = posX; + size_t y = posY; + + size_t lineWidth = 0; + size_t lineStart = 0; + size_t lastSpace = -1; + + switch (gt->vAlign) { + case GUI_TEXT_V_ALIGN_TOP: + y = posY; + break; + case GUI_TEXT_V_ALIGN_MIDDLE: + y = posY - gt->height / 2; + break; + case GUI_TEXT_V_ALIGN_BOTTOM: + y = posY - gt->height; + break; + } + + size_t yStart = y; + + glSetActiveTexture(gt->textureId); + glPolyFmt(POLY_ALPHA(31) | POLY_CULL_NONE | POLY_ID(1)); + glColor(gt->color); + + for (size_t i = 0; i < gt->length; i++) { + glImage currChar = gt->font[gt->text[i] - 1]; + lineWidth += currChar.width + 1; + + if (gt->text[i] == ' ') + lastSpace = i; + + if (gt->maxWidth && (lineWidth > gt->maxWidth || gt->text[i] == '\n')) { + if (lineWidth > gt->maxWidth) { + if (lastSpace != -1 && lastSpace > lineStart) { + i = lastSpace; + lineWidth = 0; + + for (size_t j = lineStart; j <= lastSpace; j++) + lineWidth += gt->font[gt->text[j] - 1].width + 1; + + } else { + lineWidth -= currChar.width + 1; + i--; + } + } + + switch (gt->hAlign) { + case GUI_TEXT_H_ALIGN_LEFT: + x = posX; + break; + case GUI_TEXT_H_ALIGN_CENTER: + x = posX - lineWidth / 2; + break; + case GUI_TEXT_H_ALIGN_RIGHT: + x = posX - lineWidth; + break; + } + + for (size_t j = lineStart; j <= i; j++) { + currChar = gt->font[gt->text[j] - 1]; + + if (gt->text[j] != '\n') { + glSprite(x, y, GL_FLIP_NONE, &currChar); + x += currChar.width + 1; + } + } + + lineStart = i + 1; + lineWidth = 0; + y += currChar.height + 1; + lastSpace = -1; + + if (!gt->wrap || (gt->maxHeight && (y - yStart) + currChar.height > gt->maxHeight)) + return; + } + } + + if (lineStart < gt->length) { + switch (gt->hAlign) { + case GUI_TEXT_H_ALIGN_LEFT: + x = posX; + break; + case GUI_TEXT_H_ALIGN_CENTER: + x = posX - lineWidth / 2; + break; + case GUI_TEXT_H_ALIGN_RIGHT: + x = posX - lineWidth; + break; + } + + for (size_t j = lineStart; j < gt->length; j++) { + glImage currChar = gt->font[gt->text[j] - 1]; + glSprite(x, y, GL_FLIP_NONE, &currChar); + x += currChar.width + 1; + } + } + + glColor(RGB15(31, 31, 31)); +} + +void drawGuiText(GuiText gt) +{ + drawGuiTextPos(gt, gt->posX, gt->posY); +} diff --git a/source/gui/text.h b/source/gui/text.h new file mode 100644 index 0000000..b5df0d7 --- /dev/null +++ b/source/gui/text.h @@ -0,0 +1,40 @@ +#pragma once +#include + +typedef struct GuiText* GuiText; + +typedef enum { + GUI_TEXT_SIZE_BIG, + GUI_TEXT_SIZE_MEDIUM, + GUI_TEXT_SIZE_SMALL +} GuiTextSize; + +typedef enum { + GUI_TEXT_H_ALIGN_LEFT, + GUI_TEXT_H_ALIGN_CENTER, + GUI_TEXT_H_ALIGN_RIGHT +} GuiTextHAlign; + +typedef enum { + GUI_TEXT_V_ALIGN_TOP, + GUI_TEXT_V_ALIGN_MIDDLE, + GUI_TEXT_V_ALIGN_BOTTOM +} GuiTextVAlign; + +void initGuiFont(void); +GuiText newGuiText(const char* text, GuiTextSize, u16 color); +void freeGuiText(GuiText); +void setGuiTextText(GuiText, const char*); +void setGuiTextPos(GuiText, size_t posX, size_t posY); +void setGuiTextAlignment(GuiText, GuiTextHAlign, GuiTextVAlign); +void setGuiTextMaxWidth(GuiText, size_t); +void setGuiTextMaxHeight(GuiText, size_t); +void setGuiTextWrap(GuiText, bool); +size_t getGuiTextWidth(GuiText); +size_t getGuiTextHeight(GuiText); +size_t getGuiTextPosX(GuiText); +size_t getGuiTextPosY(GuiText); +GuiTextHAlign getGuiTextHAlignment(GuiText); +GuiTextVAlign getGuiTextVAlignment(GuiText); +void drawGuiTextPos(GuiText, size_t posX, size_t posY); +void drawGuiText(GuiText); diff --git a/source/gui/video.c b/source/gui/video.c new file mode 100644 index 0000000..2ceaae8 --- /dev/null +++ b/source/gui/video.c @@ -0,0 +1,43 @@ +#include "video.h" + +#include +#include + +void initSubSprites(void) +{ + oamInit(&oamSub, SpriteMapping_Bmp_2D_256, false); + + // Set up a 4x3 grid of 64x64 sprites to cover the screen + u8 id = 0; + for (u8 y = 0; y < 3; y++) { + for (u8 x = 0; x < 4; x++, id++) { + oamSub.oamMemory[id].attribute[0] = ATTR0_BMP | ATTR0_SQUARE | (64 * y); + oamSub.oamMemory[id].attribute[1] = ATTR1_SIZE_64 | (64 * x); + oamSub.oamMemory[id].attribute[2] = ATTR2_ALPHA(1) | (8 * 32 * y) | (8 * x); + } + } + + swiWaitForVBlank(); + oamUpdate(&oamSub); +} + +void initGuiVideo(void) +{ + videoSetMode(MODE_0_3D); + videoSetModeSub(MODE_5_2D); + + initSubSprites(); + bgInitSub(3, BgType_Bmp16, BgSize_B16_256x256, 0, 0); + + glScreen2D(); + + vramSetBankA(VRAM_A_TEXTURE); + vramSetBankB(VRAM_B_TEXTURE); + vramSetBankF(VRAM_F_TEX_PALETTE); +} + +void guiLoop(void) +{ + glFlush(0); + swiWaitForVBlank(); +} diff --git a/source/gui/video.h b/source/gui/video.h new file mode 100644 index 0000000..c3ab879 --- /dev/null +++ b/source/gui/video.h @@ -0,0 +1,4 @@ +#pragma once + +void initGuiVideo(void); +void guiLoop(void); diff --git a/source/lang/en.lang b/source/lang/en.lang new file mode 100644 index 0000000..712af15 --- /dev/null +++ b/source/lang/en.lang @@ -0,0 +1,191 @@ +msgid "Ok" +msgstr "" + +msgid "Cancel" +msgstr "" + +msgid "Yes" +msgstr "" + +msgid "No" +msgstr "" + +msgid "Browse" +msgstr "" + +msgid "Search by title" +msgstr "" + +msgid "Browse all" +msgstr "" + +msgid "No database loaded or initialized" +msgstr "" + +msgid "Page" +msgstr "" + +msgid "Previous" +msgstr "" + +msgid "Next" +msgstr "" + +msgid "Platform" +msgstr "" + +msgid "Region" +msgstr "" + +msgid "Author" +msgstr "" + +msgid "Version" +msgstr "" + +msgid "Download" +msgstr "" + +msgid "Error" +msgstr "" + +msgid "Info" +msgstr "" + +msgid "Prompt" +msgstr "" + +msgid "Download failed" +msgstr "" + +msgid "could not create download directory" +msgstr "" + +msgid "bad response status" +msgstr "" + +msgid "could not initialize download" +msgstr "" + +msgid "could not create file" +msgstr "" + +msgid "could not perform download" +msgstr "" + +msgid "Download completed" +msgstr "" + +msgid "Extracting content..." +msgstr "" + +msgid "Extraction failed" +msgstr "" + +msgid "could not open file" +msgstr "" + +msgid "could not locate file" +msgstr "" + +msgid "could not read file" +msgstr "" + +msgid "could not write file" +msgstr "" + +msgid "Do you wish to extract the content in a separate directory?" +msgstr "" + +msgid "Failed to initialize database" +msgstr "" + +msgid "download failed" +msgstr "" + +msgid "file not found" +msgstr "" + +msgid "Database open failed" +msgstr "" + +msgid "invalid version" +msgstr "" + +msgid "invalid format" +msgstr "" + +msgid "Database loaded successfully" +msgstr "" + +msgid "entries" +msgstr "" + +msgid "Databases" +msgstr "" + +msgid "No databases found" +msgstr "" + +msgid "Loading database..." +msgstr "" + +msgid "Settings" +msgstr "" + +msgid "Downloads directory" +msgstr "" + +msgid "Use platform-specific directories (E.g. nds, gba)" +msgstr "" + +msgid "Color scheme" +msgstr "" + +msgid "Check for updates on start" +msgstr "" + +msgid "Language" +msgstr "" + +msgid "Path can not be empty" +msgstr "" + +msgid "Directory does not exist. Do you want to create it?" +msgstr "" + +msgid "Failed to create directory" +msgstr "" + +msgid "English" +msgstr "" + +msgid "Italian" +msgstr "" + +msgid "Check for updates" +msgstr "" + +msgid "Could not download update" +msgstr "" + +msgid "Extracting update files..." +msgstr "" + +msgid "Could not extract update files" +msgstr "" + +msgid "Update downloaded successfully. Do you want to reboot now?" +msgstr "" + +msgid "Checking for updates..." +msgstr "" + +msgid "New version found" +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Ignore" +msgstr "" diff --git a/source/lang/it.lang b/source/lang/it.lang new file mode 100644 index 0000000..f6697b3 --- /dev/null +++ b/source/lang/it.lang @@ -0,0 +1,188 @@ +msgid "Ok" +msgstr "Ok" + +msgid "Cancel" +msgstr "Annulla" + +msgid "Yes" +msgstr "Si" + +msgid "No" +msgstr "No" + +msgid "Browse" +msgstr "Sfoglia" + +msgid "Search by title" +msgstr "Cerca per titolo" + +msgid "Browse all" +msgstr "Sfoglia tutto" + +msgid "No database loaded or initialized" +msgstr "Nessun database caricato o inizializzato" + +msgid "Page" +msgstr "Pagina" + +msgid "Previous" +msgstr "Indietro" + +msgid "Next" +msgstr "Avanti" + +msgid "Platform" +msgstr "Piattaforma" + +msgid "Region" +msgstr "Regione" + +msgid "Author" +msgstr "Autore" + +msgid "Version" +msgstr "Versione" + +msgid "Download" +msgstr "Scarica" + +msgid "Error" +msgstr "Errore" + +msgid "Info" +msgstr "Info" + +msgid "Prompt" +msgstr "Richiesta" + +msgid "Download failed" +msgstr "Download fallito" + +msgid "could not create download directory" +msgstr "impossibile creare la directory di download" + +msgid "bad response status" +msgstr "stato di risposta non valido" + +msgid "could not initialize download" +msgstr "impossibile inizializzare il download" + +msgid "could not create file" +msgstr "impossibile creare il file" + +msgid "could not perform download" +msgstr "impossibile eseguire il download" + +msgid "Download completed" +msgstr "Download completato" + +msgid "Extracting content..." +msgstr "Estraendo il contenuto..." + +msgid "Extraction failed" +msgstr "Estrazione fallita" + +msgid "could not open file" +msgstr "impossibile aprire il file" + +msgid "could not locate file" +msgstr "impossibile trovare il file" + +msgid "could not read file" +msgstr "impossibile leggere il file" + +msgid "could not write file" +msgstr "impossibile scrivere il file" + +msgid "Do you wish to extract the content in a separate directory?" +msgstr "Vuoi estrarre il contenuto in una cartella separata?" + +msgid "Failed to initialize database" +msgstr "Inizializzazione database fallita" + +msgid "download failed" +msgstr "download fallito" + +msgid "file not found" +msgstr "file non trovato" + +msgid "Database open failed" +msgstr "Apertura database fallita" + +msgid "invalid version" +msgstr "versione non valida" + +msgid "invalid format" +msgstr "formato non valido" + +msgid "Database loaded successfully" +msgstr "Database caricato correttamente" + +msgid "entries" +msgstr "entry" + +msgid "Databases" +msgstr "Database" + +msgid "No databases found" +msgstr "Nessun database trovato" + +msgid "Loading database..." +msgstr "Caricando database..." + +msgid "Settings" +msgstr "Impostazioni" + +msgid "Downloads directory" +msgstr "Cartella download" + +msgid "Use platform-specific directories (E.g. nds, gba)" +msgstr "Usa cartelle specifiche per piattaforma (Es. nds, gba)" + +msgid "Color scheme" +msgstr "Schema colore" + +msgid "Check for updates on start" +msgstr "Controlla aggiornamenti all'avvio" + +msgid "Language" +msgstr "Lingua" + +msgid "Path can not be empty" +msgstr "Il percorso non puo' essere vuoto" + +msgid "Directory does not exist. Do you want to create it?" +msgstr "La cartella non esiste. Vuoi crearla?" + +msgid "Failed to create directory" +msgstr "Impossibile creare la cartella" + +msgid "Italian" +msgstr "Italiano" + +msgid "Check for updates" +msgstr "Controlla aggiornamenti" + +msgid "Could not download update" +msgstr "Impossibile scaricare aggiornamento" + +msgid "Extracting update files..." +msgstr "Estraendo file di aggiornamento..." + +msgid "Could not extract update files" +msgstr "Impossibile estrarre file di aggiornamento" + +msgid "Update downloaded successfully. Do you want to reboot now?" +msgstr "Aggiornamento scaricato con successo. Vuoi riavviare adesso?" + +msgid "Checking for updates..." +msgstr "Controllando aggiornamenti..." + +msgid "New version found" +msgstr "Nuova versione trovata" + +msgid "Update" +msgstr "Aggiorna" + +msgid "Ignore" +msgstr "Ignora" diff --git a/source/main.c b/source/main.c new file mode 100644 index 0000000..70729f5 --- /dev/null +++ b/source/main.c @@ -0,0 +1,50 @@ +#include "main.h" + +#include "config.h" +#include "gui/text.h" +#include "gui/video.h" +#include "menu.h" +#include "networking.h" +#include "settings.h" +#include "utils/filesystem.h" +#include +#include +#include + +Database db = NULL; + +void handleInit(bool result, const char* infoMessage, const char* errorMessage, u8 sleepTime, bool exitOnError) +{ + iprintf(infoMessage); + + if (result) { + iprintf("Ok!\n"); + return; + } + + iprintf(errorMessage); + sleep(sleepTime); + + if (exitOnError) + exit(1); +} + +int main(void) +{ + consoleDemoInit(); + + iprintf("\n\t\t\t" APP_NAME " " APP_VERSION "\n\n"); + iprintf("Initializing\n\n"); + + handleInit(fatInitDefault(), "Filesystem...", "\n\nFailed to initialize filesystem\n", 5, true); + handleInit(initNetworking() == NETWORKING_INIT_SUCCESS, "Networking...", "\n\nFailed to initialize networking:\nwifi connection failed\n", 5, true); + handleInit((createDir(APPDATA_DIR) && createDir(CACHE_DIR)), "Directories...", "\n\nFailed to initialize directories\n", 5, true); + handleInit((defaultSettings() && loadSettings()), "Settings...", "\n\nLoaded default settings\n", 3, false); + + initGuiVideo(); + initGuiFont(); + + menuBegin(MENU_BROWSE); + + return 0; +} diff --git a/source/main.h b/source/main.h new file mode 100644 index 0000000..16e5aa9 --- /dev/null +++ b/source/main.h @@ -0,0 +1,4 @@ +#pragma once +#include "database.h" + +extern Database db; diff --git a/source/menu.c b/source/menu.c new file mode 100644 index 0000000..fd31350 --- /dev/null +++ b/source/menu.c @@ -0,0 +1,1835 @@ +#include "menu.h" + +#include "archives.h" +#include "brickColor1.h" +#include "brickColor2.h" +#include "colors.h" +#include "config.h" +#include "gettext.h" +#include "gui/box.h" +#include "gui/button.h" +#include "gui/image.h" +#include "gui/input.h" +#include "gui/keyboard.h" +#include "gui/progressbar.h" +#include "gui/screen.h" +#include "gui/text.h" +#include "gui/video.h" +#include "lButton.h" +#include "main.h" +#include "navbarBrowseIcon.h" +#include "navbarDatabasesIcon.h" +#include "navbarInfoIcon.h" +#include "navbarSettingsIcon.h" +#include "networking.h" +#include "rButton.h" +#include "settings.h" +#include "utils/filesystem.h" +#include "utils/math.h" +#include "utils/strings.h" +#include +#include + +// Current menu +MenuEnum menu; + +// Choose color macro +#define col(x) colorSchemes[settings.colorScheme][x] + +// Translate string macro +#define tr(x) gettext(x) + +// Search parameters +char searchTitle[128]; +size_t searchPage = 1; + +Entry selectedEntry; + +#define NAVBAR_BUTTONS_COUNT 4 + +struct Navbar { + GuiBox bg; + GuiBox btnBg; + GuiBox btnBgHover; + GuiImage btnIcons[NAVBAR_BUTTONS_COUNT]; + GuiImage btnIconsHover[NAVBAR_BUTTONS_COUNT]; + GuiButton btns[NAVBAR_BUTTONS_COUNT]; +} navbar; + +// Initializes the navbar with buttons and icons +void initNavbar(void) +{ + navbar.bg = newGuiBox(SCREEN_WIDTH, 32, col(COLOR_SECONDARY)); + setGuiBoxPos(navbar.bg, 0, 160); + + navbar.btnBg = newGuiBox(64, 32, col(COLOR_SECONDARY)); + navbar.btnBgHover = newGuiBox(64, 32, col(COLOR_PRIMARY)); + + navbar.btnIcons[0] = newGuiImage(navbarBrowseIconBitmap, navbarBrowseIconPal, 20, 20, 32, 32, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + navbar.btnIcons[1] = newGuiImage(navbarDatabasesIconBitmap, navbarDatabasesIconPal, 20, 20, 32, 32, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + navbar.btnIcons[2] = newGuiImage(navbarSettingsIconBitmap, navbarSettingsIconPal, 20, 20, 32, 32, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + navbar.btnIcons[3] = newGuiImage(navbarInfoIconBitmap, navbarInfoIconPal, 20, 20, 32, 32, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + + navbar.btnIconsHover[0] = newGuiImage(navbarBrowseIconBitmap, navbarBrowseIconPal, 20, 20, 32, 32, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + navbar.btnIconsHover[1] = newGuiImage(navbarDatabasesIconBitmap, navbarDatabasesIconPal, 20, 20, 32, 32, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + navbar.btnIconsHover[2] = newGuiImage(navbarSettingsIconBitmap, navbarSettingsIconPal, 20, 20, 32, 32, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + navbar.btnIconsHover[3] = newGuiImage(navbarInfoIconBitmap, navbarInfoIconPal, 20, 20, 32, 32, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + + for (size_t i = 0; i < NAVBAR_BUTTONS_COUNT; i++) { + setGuiImageColorTint(navbar.btnIcons[i], col(COLOR_PRIMARY)); + setGuiImageColorTint(navbar.btnIconsHover[i], col(COLOR_SECONDARY)); + + navbar.btns[i] = newGuiButton(64, 32); + setGuiButtonBg(navbar.btns[i], navbar.btnBg, navbar.btnBgHover); + setGuiButtonIcon(navbar.btns[i], navbar.btnIcons[i], navbar.btnIconsHover[i]); + setGuiButtonPos(navbar.btns[i], i * 64, 160); + } +} + +// Frees the resources allocated for the navbar +void freeNavbar(void) +{ + freeGuiBox(navbar.bg); + freeGuiBox(navbar.btnBgHover); + freeGuiImage(navbar.btnIcons[0]); + freeGuiImage(navbar.btnIcons[1]); + freeGuiImage(navbar.btnIcons[2]); + freeGuiImage(navbar.btnIcons[3]); + freeGuiImage(navbar.btnIconsHover[0]); + freeGuiImage(navbar.btnIconsHover[1]); + freeGuiImage(navbar.btnIconsHover[2]); + freeGuiImage(navbar.btnIconsHover[3]); + + for (size_t i = 0; i < NAVBAR_BUTTONS_COUNT; i++) + freeGuiButton(navbar.btns[i]); +} + +// Switches the current menu and updates the navbar buttons accordingly +void switchMenu(MenuEnum targetMenu) +{ + // Set navbar buttons to original state + for (size_t i = 0; i < NAVBAR_BUTTONS_COUNT; i++) { + setGuiButtonBg(navbar.btns[i], navbar.btnBg, navbar.btnBgHover); + setGuiButtonIcon(navbar.btns[i], navbar.btnIcons[i], navbar.btnIconsHover[i]); + resetGuiButtonState(navbar.btns[i]); + } + + switch (targetMenu) { + case MENU_BROWSE: + setGuiButtonBg(navbar.btns[0], navbar.btnBgHover, navbar.btnBgHover); + setGuiButtonIcon(navbar.btns[0], navbar.btnIconsHover[0], navbar.btnIconsHover[0]); + break; + case MENU_DATABASES: + setGuiButtonBg(navbar.btns[1], navbar.btnBgHover, navbar.btnBgHover); + setGuiButtonIcon(navbar.btns[1], navbar.btnIconsHover[1], navbar.btnIconsHover[1]); + break; + case MENU_SETTINGS: + setGuiButtonBg(navbar.btns[2], navbar.btnBgHover, navbar.btnBgHover); + setGuiButtonIcon(navbar.btns[2], navbar.btnIconsHover[2], navbar.btnIconsHover[2]); + break; + case MENU_INFO: + setGuiButtonBg(navbar.btns[3], navbar.btnBgHover, navbar.btnBgHover); + setGuiButtonIcon(navbar.btns[3], navbar.btnIconsHover[3], navbar.btnIconsHover[3]); + break; + default: + break; + } + + menu = targetMenu; +} + +// Switches the menu based on which navbar button has been clicked +void navbarSwitchMenu(void) +{ + for (size_t i = 0; i < NAVBAR_BUTTONS_COUNT; i++) { + if (getGuiButtonState(navbar.btns[i]) != GUI_BUTTON_STATE_CLICKED) + continue; + + switch (i) { + case 0: + switchMenu(MENU_BROWSE); + break; + case 1: + switchMenu(MENU_DATABASES); + break; + case 2: + switchMenu(MENU_SETTINGS); + break; + case 3: + switchMenu(MENU_INFO); + break; + } + + resetGuiButtonState(navbar.btns[i]); + } +} + +// Adds the navbar elements to the given screen +void addNavbarToGuiScreen(GuiScreen gs) +{ + addToGuiScreen(gs, navbar.bg, GUI_ELEMENT_TYPE_BOX); + + for (size_t i = 0; i < NAVBAR_BUTTONS_COUNT; i++) + addToGuiScreen(gs, navbar.btns[i], GUI_ELEMENT_TYPE_BUTTON); +} + +// Displays a keyboard interface to edit the given string +// Returns true if the changes to the string have been saved, false if ignored +bool keyboardEdit(char* str, size_t maxLength) +{ + char tempStr[maxLength]; + strncpy(tempStr, str, maxLength); + + size_t pos = strlen(tempStr); // Char write position + + GuiScreen topScreen = newGuiScreen(GUI_SCREEN_LCD_TOP); + GuiScreen bottomScreen = newGuiScreen(GUI_SCREEN_LCD_BOTTOM); + + GuiBox topBg = newGuiBox(SCREEN_WIDTH, SCREEN_HEIGHT, col(COLOR_BG)); + GuiBox bottomBg = newGuiBox(SCREEN_WIDTH, SCREEN_HEIGHT, col(COLOR_BG_2)); + GuiBox buttonsBg = newGuiBox(SCREEN_WIDTH, 50, col(COLOR_SECONDARY)); + setGuiBoxPos(buttonsBg, 0, 142); + + GuiBox textboxBg = newGuiBox(224, 80, col(COLOR_BG_2)); + setGuiBoxBorder(textboxBg, 2, col(COLOR_PRIMARY)); + setGuiBoxPos(textboxBg, 16, 56); + + GuiText textboxTxt = newGuiText(tempStr, GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT_2)); + setGuiTextPos(textboxTxt, 128, 96); + setGuiTextAlignment(textboxTxt, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + setGuiTextMaxWidth(textboxTxt, 214); + setGuiTextMaxHeight(textboxTxt, 64); + setGuiTextWrap(textboxTxt, true); + + GuiKeyboard keyboard = newGuiKeyboard(col(COLOR_TEXT_2), col(COLOR_PRIMARY)); + setGuiKeyboardPos(keyboard, 0, 16); + + GuiBox btnBg = newGuiBox(70, 28, col(COLOR_BG_2)); + setGuiBoxBorder(btnBg, 2, col(COLOR_PRIMARY)); + GuiBox btnBgHover = newGuiBox(70, 28, col(COLOR_PRIMARY)); + + GuiText btnTxt[2]; + btnTxt[0] = newGuiText(tr("Ok"), GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT_2)); + btnTxt[1] = newGuiText(tr("Cancel"), GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT_2)); + setGuiTextAlignment(btnTxt[0], GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + setGuiTextAlignment(btnTxt[1], GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + GuiButton btn[2]; + btn[0] = newGuiButton(70, 28); + btn[1] = newGuiButton(70, 28); + setGuiButtonBg(btn[0], btnBg, btnBgHover); + setGuiButtonBg(btn[1], btnBg, btnBgHover); + setGuiButtonLabel(btn[0], btnTxt[0]); + setGuiButtonLabel(btn[1], btnTxt[1]); + setGuiButtonPos(btn[0], 45, 152); + setGuiButtonPos(btn[1], 141, 152); + + addToGuiScreen(topScreen, topBg, GUI_ELEMENT_TYPE_BOX); + addToGuiScreen(topScreen, textboxBg, GUI_ELEMENT_TYPE_BOX); + addToGuiScreen(topScreen, textboxTxt, GUI_ELEMENT_TYPE_TEXT); + + addToGuiScreen(bottomScreen, bottomBg, GUI_ELEMENT_TYPE_BOX); + addToGuiScreen(bottomScreen, keyboard, GUI_ELEMENT_TYPE_KEYBOARD); + addToGuiScreen(bottomScreen, buttonsBg, GUI_ELEMENT_TYPE_BOX); + addToGuiScreen(bottomScreen, btn[0], GUI_ELEMENT_TYPE_BUTTON); + addToGuiScreen(bottomScreen, btn[1], GUI_ELEMENT_TYPE_BUTTON); + + GuiScreen prevTopScreen = getActiveTopScreen(); + GuiScreen prevBottomScreen = getActiveBottomScreen(); + + setActiveScreens(topScreen, bottomScreen); + + bool save = false; + while (pmMainLoop()) { + guiLoop(); + drawScreens(); + + struct GuiKeyboardKey key = getGuiKeyboardPressed(keyboard); + + if (key.c && pos + 1 <= maxLength) { + tempStr[pos] = key.c; + tempStr[++pos] = '\0'; + setGuiTextText(textboxTxt, tempStr); + } else if (key.extraKey == GUI_KEYBOARD_EXTRA_KEY_BACKSPACE) { + if (pos > 0) + tempStr[--pos] = '\0'; + setGuiTextText(textboxTxt, tempStr); + } + + if (getGuiButtonState(btn[0]) == GUI_BUTTON_STATE_CLICKED) { + save = true; + strncpy(str, tempStr, maxLength - 1); + break; + } else if (getGuiButtonState(btn[1]) == GUI_BUTTON_STATE_CLICKED) { + save = false; + break; + } + } + + freeGuiScreen(topScreen); + freeGuiScreen(bottomScreen); + freeGuiBox(topBg); + freeGuiBox(bottomBg); + freeGuiBox(buttonsBg); + freeGuiBox(textboxBg); + freeGuiText(textboxTxt); + freeGuiKeyboard(keyboard); + freeGuiBox(btnBg); + freeGuiBox(btnBgHover); + freeGuiText(btnTxt[0]); + freeGuiText(btnTxt[1]); + freeGuiButton(btn[0]); + freeGuiButton(btn[1]); + + setActiveScreens(prevTopScreen, prevBottomScreen); + + return save; +} + +// Displays a prompt window with the given title and message +// Returns true if the first option is clicked, false if the second option is clicked +bool windowPrompt(const char* title, const char* message, const char* btn1Label, const char* btn2Label) +{ + GuiScreen bottomScreen = newGuiScreen(GUI_SCREEN_LCD_BOTTOM); + + GuiBox bg = newGuiBox(SCREEN_WIDTH, SCREEN_HEIGHT, col(COLOR_BG)); + + GuiBox promptBg = newGuiBox(244, 132, col(COLOR_BG_2)); + setGuiBoxBorder(promptBg, 2, col(COLOR_PRIMARY)); + setGuiBoxPos(promptBg, 6, 6); + + GuiText titleTxt = newGuiText(title, GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT)); + setGuiTextPos(titleTxt, 128, 25); + setGuiTextAlignment(titleTxt, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + GuiText messageTxt = newGuiText(message, GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT_2)); + setGuiTextPos(messageTxt, 128, 78); + setGuiTextAlignment(messageTxt, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + setGuiTextMaxWidth(messageTxt, 224); + setGuiTextMaxHeight(messageTxt, 100); + setGuiTextWrap(messageTxt, true); + + GuiText btn1Txt = NULL; + GuiText btn2Txt = NULL; + GuiBox btnBg1 = NULL; + GuiBox btnBg1Hover = NULL; + GuiBox btnBg2 = NULL; + GuiBox btnBg2Hover = NULL; + GuiButton btn1 = NULL; + GuiButton btn2 = NULL; + + btn1Txt = newGuiText(btn1Label, GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT_2)); + setGuiTextAlignment(btn1Txt, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + size_t btn1Width = max(70, getGuiTextWidth(btn1Txt) + 20); + + btnBg1 = newGuiBox(btn1Width, 28, col(COLOR_BG_2)); + setGuiBoxBorder(btnBg1, 2, col(COLOR_PRIMARY)); + btnBg1Hover = newGuiBox(btn1Width, 28, col(COLOR_PRIMARY)); + + btn1 = newGuiButton(btn1Width, 28); + setGuiButtonBg(btn1, btnBg1, btnBg1Hover); + setGuiButtonLabel(btn1, btn1Txt); + setGuiButtonPos(btn1, 93, 152); + + if (btn2Label) { + btn2Txt = newGuiText(btn2Label, GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT_2)); + setGuiTextAlignment(btn2Txt, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + size_t btn2Width = max(70, getGuiTextWidth(btn2Txt) + 20); + + btnBg2 = newGuiBox(btn2Width, 28, col(COLOR_BG_2)); + setGuiBoxBorder(btnBg2, 2, col(COLOR_PRIMARY)); + btnBg2Hover = newGuiBox(btn2Width, 28, col(COLOR_PRIMARY)); + + btn2 = newGuiButton(btn2Width, 28); + setGuiButtonBg(btn2, btnBg2, btnBg2Hover); + setGuiButtonLabel(btn2, btn2Txt); + + setGuiButtonPos(btn1, 80 - btn1Width / 2, 152); + setGuiButtonPos(btn2, 176 - btn2Width / 2, 152); + } + + addToGuiScreen(bottomScreen, bg, GUI_ELEMENT_TYPE_BOX); + addToGuiScreen(bottomScreen, promptBg, GUI_ELEMENT_TYPE_BOX); + addToGuiScreen(bottomScreen, titleTxt, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, messageTxt, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, btn1, GUI_ELEMENT_TYPE_BUTTON); + if (btn2Label) + addToGuiScreen(bottomScreen, btn2, GUI_ELEMENT_TYPE_BUTTON); + + GuiScreen prevTopScreen = getActiveTopScreen(); + GuiScreen prevBottomScreen = getActiveBottomScreen(); + + setActiveScreens(NULL, bottomScreen); + + bool ret = true; + while (pmMainLoop()) { + guiLoop(); + drawScreens(); + + if (getGuiButtonState(btn1) == GUI_BUTTON_STATE_CLICKED) + break; + + if (btn2Label && getGuiButtonState(btn2) == GUI_BUTTON_STATE_CLICKED) { + ret = false; + break; + } + } + + freeGuiScreen(bottomScreen); + freeGuiBox(bg); + freeGuiBox(promptBg); + freeGuiText(titleTxt); + freeGuiText(messageTxt); + freeGuiText(btn1Txt); + freeGuiBox(btnBg1); + freeGuiBox(btnBg1Hover); + freeGuiButton(btn1); + + if (btn2Label) { + freeGuiText(btn2Txt); + freeGuiBox(btnBg2); + freeGuiBox(btnBg2Hover); + freeGuiButton(btn2); + } + + setActiveScreens(prevTopScreen, prevBottomScreen); + + return ret; +} + +// Displays a message on the bottom screen in a non-blocking way +void showMessage(const char* message) +{ + GuiScreen bottomScreen = newGuiScreen(GUI_SCREEN_LCD_BOTTOM); + + GuiBox bg = newGuiBox(SCREEN_WIDTH, SCREEN_HEIGHT, col(COLOR_BG)); + + GuiText messageTxt = newGuiText(message, GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT)); + setGuiTextPos(messageTxt, 128, 96); + setGuiTextAlignment(messageTxt, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + setGuiTextMaxWidth(messageTxt, 244); + setGuiTextWrap(messageTxt, true); + + addToGuiScreen(bottomScreen, bg, GUI_ELEMENT_TYPE_BOX); + addToGuiScreen(bottomScreen, messageTxt, GUI_ELEMENT_TYPE_TEXT); + + GuiScreen prevTopScreen = getActiveTopScreen(); + GuiScreen prevBottomScreen = getActiveBottomScreen(); + + setActiveScreens(NULL, bottomScreen); + + // Make sure to draw bottom screen + guiLoop(); + drawScreens(); + guiLoop(); + drawScreens(); + + freeGuiScreen(bottomScreen); + freeGuiBox(bg); + freeGuiText(messageTxt); + + setActiveScreens(prevTopScreen, prevBottomScreen); +} + +// Draws a black background on both screens +void drawBlank(void) +{ + GuiScreen topScreen = newGuiScreen(GUI_SCREEN_LCD_TOP); + GuiScreen bottomScreen = newGuiScreen(GUI_SCREEN_LCD_BOTTOM); + + GuiBox bg = newGuiBox(SCREEN_WIDTH, SCREEN_HEIGHT, 0); + + addToGuiScreen(topScreen, bg, GUI_ELEMENT_TYPE_BOX); + addToGuiScreen(bottomScreen, bg, GUI_ELEMENT_TYPE_BOX); + + setActiveScreens(topScreen, bottomScreen); + + // Make sure to draw both screens + guiLoop(); + drawScreens(); + guiLoop(); + drawScreens(); + + freeGuiScreen(topScreen); + freeGuiScreen(bottomScreen); + freeGuiBox(bg); +} + +// Downloads and loads a box art file of a given entry into a new GuiImage element +// Returns the created GuiImage element +GuiImage loadBoxart(Entry e) +{ + char boxartFilename[NAME_MAX]; + strncpy(boxartFilename, getEntryBoxartUrl(e), sizeof(boxartFilename) - 1); + safeStr(boxartFilename); + + char boxartPath[PATH_MAX]; + joinPath(boxartPath, CACHE_DIR, boxartFilename); + + if (!fileExists(boxartPath)) + downloadFile(boxartPath, getEntryBoxartUrl(e), NULL); + + return newGuiImagePNG(boxartPath, 144, 144); +} + +// Data and elements used dynamically by the downloadProgressCallback function +struct DlData { + GuiText fileNameTxt; + GuiProgressbar progressBar; + GuiText percentTxt; + GuiText speedTxt; + GuiText progressTxt; + GuiButton cancelBtn; + time_t lastSpeedUpdateTime; + size_t lastDlnow; +} dlData; + +// curl download progress function +// Draws the download progress interface +size_t downloadProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) +{ + guiLoop(); + + char humanSizeDlnow[16]; + char humanSizeDltotal[16]; + + humanizeSize(humanSizeDlnow, sizeof(humanSizeDlnow), dlnow); + humanizeSize(humanSizeDltotal, sizeof(humanSizeDltotal), dltotal); + + char progressText[64]; + char percentText[16]; + + sprintf(progressText, "%s / %s", humanSizeDlnow, humanSizeDltotal); + + size_t percent = 0; + if (dltotal) { + percent = (double)dlnow / dltotal * 100; + sprintf(percentText, "%d %%", percent); + } else { + percentText[0] = '\0'; + } + + setGuiProgressbarPercent(dlData.progressBar, percent); + setGuiTextText(dlData.percentTxt, percentText); + setGuiTextText(dlData.progressTxt, progressText); + + time_t currentTime = time(NULL); + + // Update speed if 1 second elapsed + if ((currentTime - dlData.lastSpeedUpdateTime >= 1)) { + char humanSizeSpeed[16]; + humanizeSize(humanSizeSpeed, sizeof(humanSizeSpeed), dlnow - dlData.lastDlnow); + + char speedText[32]; + snprintf(speedText, sizeof(speedText), "%s/s", humanSizeSpeed); + + setGuiTextText(dlData.speedTxt, speedText); + dlData.lastSpeedUpdateTime = currentTime; + dlData.lastDlnow = dlnow; + } + + drawScreens(); + + if (getGuiButtonState(dlData.cancelBtn) == GUI_BUTTON_STATE_CLICKED) { + resetGuiButtonState(dlData.cancelBtn); + stopDownload(); + } + + return 0; +} + +// Sets up the download progress interface and starts the download +// Returns the download status +DownloadStatus downloadGui(const char* path, const char* url, const char* fileName) +{ + GuiScreen bottomScreen = newGuiScreen(GUI_SCREEN_LCD_BOTTOM); + + GuiBox bg = newGuiBox(SCREEN_WIDTH, SCREEN_HEIGHT, col(COLOR_BG)); + + dlData.fileNameTxt = newGuiText(fileName, GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT_2)); + setGuiTextPos(dlData.fileNameTxt, 14, 56); + setGuiTextAlignment(dlData.fileNameTxt, GUI_TEXT_H_ALIGN_LEFT, GUI_TEXT_V_ALIGN_BOTTOM); + setGuiTextMaxWidth(dlData.fileNameTxt, 216); + setGuiTextWrap(dlData.fileNameTxt, true); + setGuiTextMaxHeight(dlData.fileNameTxt, 60); + + GuiBox progressBarBg = newGuiBox(228, 26, col(COLOR_BG_2)); + setGuiBoxBorder(progressBarBg, 2, col(COLOR_PRIMARY)); + setGuiBoxPos(progressBarBg, 12, 60); + dlData.progressBar = newGuiProgressbar(224, 22, col(COLOR_BG_2), col(COLOR_PRIMARY)); + setGuiProgressbarPos(dlData.progressBar, 14, 62); + + dlData.percentTxt = newGuiText("", GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT_2)); + setGuiTextPos(dlData.percentTxt, 128, 73); + setGuiTextAlignment(dlData.percentTxt, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + dlData.speedTxt = newGuiText("", GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT_2)); + setGuiTextPos(dlData.speedTxt, 14, 92); + + dlData.progressTxt = newGuiText("", GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT_2)); + setGuiTextPos(dlData.progressTxt, 242, 92); + setGuiTextAlignment(dlData.progressTxt, GUI_TEXT_H_ALIGN_RIGHT, GUI_TEXT_V_ALIGN_TOP); + + GuiBox cancelBtnBg = newGuiBox(90, 34, col(COLOR_BG_2)); + setGuiBoxBorder(cancelBtnBg, 2, col(COLOR_PRIMARY)); + GuiBox cancelBtnBgHover = newGuiBox(90, 34, col(COLOR_PRIMARY)); + GuiText cancelBtnTxt = newGuiText(tr("Cancel"), GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT_2)); + setGuiTextAlignment(cancelBtnTxt, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + dlData.cancelBtn = newGuiButton(90, 34); + setGuiButtonPos(dlData.cancelBtn, 83, 124); + setGuiButtonBg(dlData.cancelBtn, cancelBtnBg, cancelBtnBgHover); + setGuiButtonLabel(dlData.cancelBtn, cancelBtnTxt); + + addToGuiScreen(bottomScreen, bg, GUI_ELEMENT_TYPE_BOX); + addToGuiScreen(bottomScreen, dlData.fileNameTxt, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, progressBarBg, GUI_ELEMENT_TYPE_BOX); + addToGuiScreen(bottomScreen, dlData.progressBar, GUI_ELEMENT_TYPE_PROGRESSBAR); + addToGuiScreen(bottomScreen, dlData.percentTxt, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, dlData.speedTxt, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, dlData.progressTxt, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, dlData.cancelBtn, GUI_ELEMENT_TYPE_BUTTON); + + setActiveScreens(NULL, bottomScreen); + + dlData.lastSpeedUpdateTime = 0; + dlData.lastDlnow = 0; + DownloadStatus dlStatus = downloadFile(path, url, downloadProgressCallback); + + freeGuiScreen(bottomScreen); + freeGuiBox(bg); + freeGuiText(dlData.fileNameTxt); + freeGuiBox(progressBarBg); + freeGuiProgressbar(dlData.progressBar); + freeGuiText(dlData.percentTxt); + freeGuiText(dlData.speedTxt); + freeGuiText(dlData.progressTxt); + freeGuiBox(cancelBtnBg); + freeGuiBox(cancelBtnBgHover); + freeGuiText(cancelBtnTxt); + freeGuiButton(dlData.cancelBtn); + + return dlStatus; +} + +// Handles the download of the given entry +void downloadEntry(Entry e) +{ + char downloadDirPath[PATH_MAX]; + char downloadFilePath[PATH_MAX]; + char tempMessage[128]; + + if (settings.dlUseDirs) + joinPath(downloadDirPath, settings.dlPath, getEntryPlatform(e)); + else + strcpy(downloadDirPath, settings.dlPath); + + joinPath(downloadFilePath, downloadDirPath, getEntryFileName(e)); + + if (!createDir(downloadDirPath)) { + snprintf(tempMessage, sizeof(tempMessage), "%s: %s", tr("Download failed"), tr("could not create download directory")); + windowPrompt(tr("Error"), tempMessage, tr("Ok"), NULL); + return; + } + + DownloadStatus dlStatus = downloadGui(downloadFilePath, getEntryUrl(e), getEntryFileName(e)); + + switch (dlStatus) { + case DOWNLOAD_STOPPED: + return; + case DOWNLOAD_ERR_NOT_OK: + snprintf(tempMessage, sizeof(tempMessage), "%s: %s", tr("Download failed"), tr("bad response status")); + windowPrompt(tr("Error"), tempMessage, tr("Ok"), NULL); + return; + case DOWNLOAD_ERR_INIT_FAILED: + snprintf(tempMessage, sizeof(tempMessage), "%s: %s", tr("Download failed"), tr("could not initialize download")); + windowPrompt(tr("Error"), tempMessage, tr("Ok"), NULL); + return; + case DOWNLOAD_ERR_FILE_OPEN_FAILED: + snprintf(tempMessage, sizeof(tempMessage), "%s: %s", tr("Download failed"), tr("could not create file")); + windowPrompt(tr("Error"), tempMessage, tr("Ok"), NULL); + return; + case DOWNLOAD_ERR_PERFORM_FAILED: + snprintf(tempMessage, sizeof(tempMessage), "%s: %s", tr("Download failed"), tr("could not perform download")); + windowPrompt(tr("Error"), tempMessage, tr("Ok"), NULL); + return; + default: + break; + } + + if (!fileIsZip(downloadFilePath)) { + snprintf(tempMessage, sizeof(tempMessage), "%s\n(%s)", tr("Download completed"), getEntryTitle(e)); + windowPrompt(tr("Info"), tempMessage, tr("Ok"), NULL); + return; + } + + ExtractStatus extractStatus; + char tempExtractPath[PATH_MAX]; + + size_t extractItemsCount = getEntryExtractItemsCount(e); + if (extractItemsCount) { + struct EntryExtractItem* extractItems = getEntryExtractItems(e); + for (size_t i = 0; i < extractItemsCount; i++) { + joinPath(tempExtractPath, downloadDirPath, extractItems[i].outPath); + + snprintf(tempMessage, sizeof(tempMessage), "%s (%s)", tr("Extracting content..."), extractItems[i].inPath); + showMessage(tempMessage); + + extractStatus = extractZip(downloadFilePath, extractItems[i].inPath, tempExtractPath); + + switch (extractStatus) { + case EXTRACT_ERR_FILE_OPEN: + snprintf(tempMessage, sizeof(tempMessage), "%s: %s (%s)", tr("Extraction failed"), tr("could not open file"), extractItems[i].inPath); + windowPrompt(tr("Error"), tempMessage, tr("Ok"), NULL); + break; + case EXTRACT_ERR_FILE_NOT_FOUND: + snprintf(tempMessage, sizeof(tempMessage), "%s: %s (%s)", tr("Extraction failed"), tr("could not locate file"), extractItems[i].inPath); + windowPrompt(tr("Error"), tempMessage, tr("Ok"), NULL); + break; + case EXTRACT_ERR_FILE_READ: + snprintf(tempMessage, sizeof(tempMessage), "%s: %s (%s)", tr("Extraction failed"), tr("could not read file"), extractItems[i].inPath); + windowPrompt(tr("Error"), tempMessage, tr("Ok"), NULL); + break; + case EXTRACT_ERR_FILE_WRITE: + snprintf(tempMessage, sizeof(tempMessage), "%s: %s (%s)", tr("Extraction failed"), tr("could not write file"), extractItems[i].inPath); + windowPrompt(tr("Error"), tempMessage, tr("Ok"), NULL); + break; + default: + break; + } + + if (extractStatus != EXTRACT_SUCCESS) { + for (size_t j = 0; j < extractItemsCount; j++) + deletePath(extractItems[j].outPath); + + break; + } + } + } else { + char* tempDirName = strdup(getEntryTitle(e)); + safeStr(tempDirName); + + joinPath(tempExtractPath, downloadDirPath, tempDirName); + + free(tempDirName); + + snprintf(tempMessage, sizeof(tempMessage), "%s (%.124s)", tr("Do you wish to extract the content in a separate directory?"), tempExtractPath); + + if (!windowPrompt(tr("Prompt"), tempMessage, tr("Yes"), tr("No"))) + strcpy(tempExtractPath, downloadDirPath); + + snprintf(tempMessage, sizeof(tempMessage), "%s (%.124s)", tr("Extracting content..."), tempExtractPath); + showMessage(tempMessage); + + extractStatus = extractAllZip(downloadFilePath, tempExtractPath); + + switch (extractStatus) { + case EXTRACT_ERR_FILE_OPEN: + snprintf(tempMessage, sizeof(tempMessage), "%s: %s", tr("Extraction failed"), tr("could not open file")); + windowPrompt(tr("Error"), tempMessage, tr("Ok"), NULL); + break; + case EXTRACT_ERR_FILE_READ: + snprintf(tempMessage, sizeof(tempMessage), "%s: %s", tr("Extraction failed"), tr("could not read file")); + windowPrompt(tr("Error"), tempMessage, tr("Ok"), NULL); + break; + case EXTRACT_ERR_FILE_WRITE: + snprintf(tempMessage, sizeof(tempMessage), "%s: %s", tr("Extraction failed"), tr("could not write file")); + windowPrompt(tr("Error"), tempMessage, tr("Ok"), NULL); + break; + default: + break; + } + } + + if (extractStatus == EXTRACT_SUCCESS) { + snprintf(tempMessage, sizeof(tempMessage), "%s\n(%s)", tr("Download completed"), getEntryTitle(e)); + windowPrompt(tr("Info"), tempMessage, tr("Ok"), NULL); + } + + deleteFile(downloadFilePath); +} + +#define UPDATE_TEMP_FILENAME "tmpUpdateFile" +#define UPDATE_FILENAME "Kekatsu.nds" + +// Handles the download of the update file +void downloadUpdate(void) +{ + char workDir[PATH_MAX]; + getcwd(workDir, sizeof(workDir)); + + char downloadFilePath[PATH_MAX]; + joinPath(downloadFilePath, workDir, UPDATE_TEMP_FILENAME); + + DownloadStatus dlStatus = downloadGui(downloadFilePath, UPDATE_URL_APP, UPDATE_FILENAME); + + if (dlStatus != DOWNLOAD_SUCCESS) { + if (dlStatus != DOWNLOAD_STOPPED) + windowPrompt(tr("Error"), tr("Could not download update"), tr("Ok"), NULL); + return; + } + + char execPath[PATH_MAX]; + joinPath(execPath, workDir, UPDATE_FILENAME); + + renamePath(downloadFilePath, execPath); + + if (windowPrompt(tr("Info"), tr("Update downloaded successfully. Do you want to reboot now?"), tr("Yes"), tr("No"))) + switchMenu(MENU_EXIT); +} + +// Handles the update check and download +void handleUpdateCheck(void) +{ + showMessage(tr("Checking for updates...")); + + char newVersion[16]; + DownloadStatus dlStatus = downloadToString(newVersion, sizeof(newVersion), UPDATE_URL_VERSION); + + if (dlStatus != DOWNLOAD_SUCCESS) + return; + + newVersion[strcspn(newVersion, "\r\n")] = '\0'; + + if (strcmp(APP_VERSION, newVersion) == 0) + return; + + char tempMessage[128]; + snprintf(tempMessage, sizeof(tempMessage), "%s: %s", tr("New version found"), newVersion); + + if (!windowPrompt(tr("Info"), tempMessage, tr("Update"), tr("Ignore"))) + return; + + downloadUpdate(); +} + +// Handles the loading of the given database +// Returns true if the database has been loaded, false if there was an error +bool loadDatabase(Database d) +{ + char tempMessage[128]; + snprintf(tempMessage, sizeof(tempMessage), "%s (%s)", tr("Loading database..."), getDatabaseName(d)); + showMessage(tempMessage); + + DatabaseInitStatus initStatus = initDatabase(d); + switch (initStatus) { + case DATABASE_INIT_ERR_DOWNLOAD: + snprintf(tempMessage, sizeof(tempMessage), "%s: %s", tr("Failed to initialize database"), tr("download failed")); + break; + case DATABASE_INIT_ERR_FILE_NOT_FOUND: + snprintf(tempMessage, sizeof(tempMessage), "%s: %s", tr("Failed to initialize database"), tr("file not found")); + break; + default: + break; + } + + if (initStatus != DATABASE_INIT_SUCCESS && initStatus != DATABASE_INIT_SUCCESS_CACHE) { + windowPrompt(tr("Error"), tempMessage, tr("Ok"), NULL); + return false; + } + + DatabaseOpenStatus openStatus = openDatabase(d); + switch (openStatus) { + case DATABASE_OPEN_ERR_FILE_OPEN: + snprintf(tempMessage, sizeof(tempMessage), "%s: %s", tr("Database open failed"), tr("could not open file")); + break; + case DATABASE_OPEN_ERR_INVALID_VERSION: + snprintf(tempMessage, sizeof(tempMessage), "%s: %s", tr("Database open failed"), tr("invalid version")); + break; + case DATABASE_OPEN_ERR_INVALID_FORMAT: + snprintf(tempMessage, sizeof(tempMessage), "%s: %s", tr("Database open failed"), tr("invalid format")); + break; + default: + break; + } + + if (openStatus != DATABASE_OPEN_SUCCESS) { + windowPrompt(tr("Error"), tempMessage, "OK", NULL); + return false; + } + + if (db) + freeDatabase(db); + db = d; + + return true; +} + +// Loads the last opened database +void loadLastOpenedDb(void) +{ + Database lastDb = getLastOpenedDatabase(); + if (!lastDb) + return; + + if (!loadDatabase(lastDb)) + freeDatabase(lastDb); +} + +// Browse menu +// Displays the search options +void browseMenu(void) +{ + menu = MENU_NONE; + + GuiScreen topScreen = newGuiScreen(GUI_SCREEN_LCD_TOP); + GuiScreen bottomScreen = newGuiScreen(GUI_SCREEN_LCD_BOTTOM); + + GuiBox bg = newGuiBox(SCREEN_WIDTH, SCREEN_HEIGHT, col(COLOR_BG)); + + GuiImage brickColor1 = newGuiImage(brickColor1Bitmap, brickColor1Pal, 64, 48, 64, 64, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + GuiImage brickColor2 = newGuiImage(brickColor2Bitmap, brickColor2Pal, 64, 48, 64, 64, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + setGuiImageColorTint(brickColor1, col(COLOR_PRIMARY)); + setGuiImageColorTint(brickColor2, col(COLOR_SECONDARY)); + setGuiImagePos(brickColor1, 96, 72); + setGuiImagePos(brickColor2, 96, 72); + + char menuTitleText[32]; + snprintf(menuTitleText, sizeof(menuTitleText), "%s (%d)", tr("Browse"), (db ? getDatabaseSize(db) : 0)); + + GuiText menuTitle = newGuiText(menuTitleText, GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT)); + setGuiTextPos(menuTitle, 128, 178); + setGuiTextAlignment(menuTitle, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + GuiBox btnBg = newGuiBox(SCREEN_WIDTH, 32, col(COLOR_BG_2)); + setGuiBoxBorder(btnBg, 2, col(COLOR_PRIMARY)); + GuiBox btnBgHover = newGuiBox(SCREEN_WIDTH, 32, col(COLOR_PRIMARY)); + + GuiText btn1Txt = newGuiText(tr("Search by title"), GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT_2)); + setGuiTextPos(btn1Txt, 12, 0); + setGuiTextAlignment(btn1Txt, GUI_TEXT_H_ALIGN_LEFT, GUI_TEXT_V_ALIGN_MIDDLE); + GuiButton btn1 = newGuiButton(SCREEN_WIDTH, 32); + setGuiButtonBg(btn1, btnBg, btnBgHover); + setGuiButtonLabel(btn1, btn1Txt); + + GuiText btn2Txt = newGuiText(tr("Browse all"), GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT_2)); + setGuiTextPos(btn2Txt, 12, 0); + setGuiTextAlignment(btn2Txt, GUI_TEXT_H_ALIGN_LEFT, GUI_TEXT_V_ALIGN_MIDDLE); + GuiButton btn2 = newGuiButton(SCREEN_WIDTH, 32); + setGuiButtonBg(btn2, btnBg, btnBgHover); + setGuiButtonLabel(btn2, btn2Txt); + setGuiButtonPos(btn2, 0, 32); + + GuiText noDatabaseTxt = newGuiText(tr("No database loaded or initialized"), GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT_3)); + setGuiTextPos(noDatabaseTxt, 128, 80); + setGuiTextAlignment(noDatabaseTxt, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + setGuiTextMaxWidth(noDatabaseTxt, 244); + setGuiTextWrap(noDatabaseTxt, true); + + addToGuiScreen(topScreen, bg, GUI_ELEMENT_TYPE_BOX); + addToGuiScreen(topScreen, brickColor1, GUI_ELEMENT_TYPE_IMAGE); + addToGuiScreen(topScreen, brickColor2, GUI_ELEMENT_TYPE_IMAGE); + addToGuiScreen(topScreen, menuTitle, GUI_ELEMENT_TYPE_TEXT); + + addToGuiScreen(bottomScreen, bg, GUI_ELEMENT_TYPE_BOX); + + if (db && getDatabaseIsInited(db)) { + addToGuiScreen(bottomScreen, btn1, GUI_ELEMENT_TYPE_BUTTON); + addToGuiScreen(bottomScreen, btn2, GUI_ELEMENT_TYPE_BUTTON); + } else { + addToGuiScreen(bottomScreen, noDatabaseTxt, GUI_ELEMENT_TYPE_TEXT); + } + + addNavbarToGuiScreen(bottomScreen); + + setActiveScreens(topScreen, bottomScreen); + while (pmMainLoop() && menu == MENU_NONE) { + guiLoop(); + drawScreens(); + + if (getGuiButtonState(btn1) == GUI_BUTTON_STATE_CLICKED) { + if (keyboardEdit(searchTitle, sizeof(searchTitle))) { + searchPage = 1; + switchMenu(MENU_RESULTS); + } + resetGuiButtonState(btn1); + } else if (getGuiButtonState(btn2) == GUI_BUTTON_STATE_CLICKED) { + searchTitle[0] = '\0'; + searchPage = 1; + switchMenu(MENU_RESULTS); + } + + navbarSwitchMenu(); + } + + freeGuiScreen(topScreen); + freeGuiScreen(bottomScreen); + freeGuiBox(bg); + freeGuiImage(brickColor1); + freeGuiImage(brickColor2); + freeGuiText(menuTitle); + freeGuiBox(btnBg); + freeGuiBox(btnBgHover); + freeGuiText(btn1Txt); + freeGuiButton(btn1); + freeGuiText(btn2Txt); + freeGuiButton(btn2); + freeGuiText(noDatabaseTxt); +} + +#define MAX_RESULTS_PER_PAGE 50 +#define MAX_RESULTS_DISPLAY 5 + +// Results menu +// Starts the search in the database with the set parameters and shows the results +void resultsMenu(void) +{ + menu = MENU_NONE; + + size_t resultsCount; + Entry* results = searchDatabase(db, searchTitle, MAX_RESULTS_PER_PAGE, searchPage, &resultsCount); + + bool prevPageEnabled = (searchPage > 1); + bool nextPageEnabled = (resultsCount == MAX_RESULTS_PER_PAGE); + + GuiScreen topScreen = newGuiScreen(GUI_SCREEN_LCD_TOP); + GuiScreen bottomScreen = newGuiScreen(GUI_SCREEN_LCD_BOTTOM); + setGuiScreenDpadNavigate(topScreen, 0); + setGuiScreenDpadNavigate(bottomScreen, 0); + + GuiBox bg = newGuiBox(SCREEN_WIDTH, SCREEN_HEIGHT, col(COLOR_BG)); + + GuiImage brickColor1 = newGuiImage(brickColor1Bitmap, brickColor1Pal, 64, 48, 64, 64, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + GuiImage brickColor2 = newGuiImage(brickColor2Bitmap, brickColor2Pal, 64, 48, 64, 64, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + setGuiImageColorTint(brickColor1, col(COLOR_PRIMARY)); + setGuiImageColorTint(brickColor2, col(COLOR_SECONDARY)); + setGuiImagePos(brickColor1, 96, 72); + setGuiImagePos(brickColor2, 96, 72); + + char menuTitleText[16]; + snprintf(menuTitleText, sizeof(menuTitleText), "%s %d (%d)", tr("Page"), searchPage, resultsCount); + GuiText menuTitle = newGuiText(menuTitleText, GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT)); + setGuiTextPos(menuTitle, 128, 178); + setGuiTextAlignment(menuTitle, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + GuiText prevPageTxt = newGuiText(tr("Previous"), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT)); + GuiText nextPageTxt = newGuiText(tr("Next"), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT)); + setGuiTextPos(prevPageTxt, 26, 180); + setGuiTextPos(nextPageTxt, 230, 180); + setGuiTextAlignment(prevPageTxt, GUI_TEXT_H_ALIGN_LEFT, GUI_TEXT_V_ALIGN_MIDDLE); + setGuiTextAlignment(nextPageTxt, GUI_TEXT_H_ALIGN_RIGHT, GUI_TEXT_V_ALIGN_MIDDLE); + + GuiImage lButtonIcon = newGuiImage(lButtonBitmap, lButtonPal, 16, 10, 16, 16, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + GuiImage rButtonIcon = newGuiImage(rButtonBitmap, rButtonPal, 16, 10, 16, 16, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + setGuiImagePos(lButtonIcon, 6, 174); + setGuiImagePos(rButtonIcon, 234, 174); + + GuiBox resultsBtnBg = newGuiBox(SCREEN_WIDTH, 32, col(COLOR_BG_2)); + setGuiBoxBorder(resultsBtnBg, 2, col(COLOR_PRIMARY)); + GuiBox resultsBtnBgHover = newGuiBox(SCREEN_WIDTH, 32, col(COLOR_PRIMARY)); + + GuiText resultsBtnTxt[resultsCount]; + GuiText resultsBtnTxtPlatform[resultsCount]; + GuiText resultsBtnTxtRegion[resultsCount]; + GuiButton resultsBtn[resultsCount]; + + for (size_t i = 0; i < resultsCount; i++) { + resultsBtnTxt[i] = newGuiText(getEntryTitle(results[i]), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_2)); + setGuiTextPos(resultsBtnTxt[i], 12, 0); + setGuiTextAlignment(resultsBtnTxt[i], GUI_TEXT_H_ALIGN_LEFT, GUI_TEXT_V_ALIGN_MIDDLE); + setGuiTextMaxWidth(resultsBtnTxt[i], 186); + setGuiTextMaxHeight(resultsBtnTxt[i], 32); + setGuiTextWrap(resultsBtnTxt[i], true); + + resultsBtnTxtPlatform[i] = newGuiText(getEntryPlatform(results[i]), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_2)); + setGuiTextAlignment(resultsBtnTxtPlatform[i], GUI_TEXT_H_ALIGN_RIGHT, GUI_TEXT_V_ALIGN_MIDDLE); + setGuiTextMaxWidth(resultsBtnTxtPlatform[i], 48); + + resultsBtnTxtRegion[i] = newGuiText(getEntryRegion(results[i]), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_2)); + setGuiTextAlignment(resultsBtnTxtRegion[i], GUI_TEXT_H_ALIGN_RIGHT, GUI_TEXT_V_ALIGN_MIDDLE); + setGuiTextMaxWidth(resultsBtnTxtRegion[i], 48); + + resultsBtn[i] = newGuiButton(SCREEN_WIDTH, 32); + setGuiButtonBg(resultsBtn[i], resultsBtnBg, resultsBtnBgHover); + setGuiButtonLabel(resultsBtn[i], resultsBtnTxt[i]); + } + + addToGuiScreen(topScreen, bg, GUI_ELEMENT_TYPE_BOX); + addToGuiScreen(topScreen, brickColor1, GUI_ELEMENT_TYPE_IMAGE); + addToGuiScreen(topScreen, brickColor2, GUI_ELEMENT_TYPE_IMAGE); + addToGuiScreen(topScreen, menuTitle, GUI_ELEMENT_TYPE_TEXT); + + if (prevPageEnabled) { + addToGuiScreen(topScreen, prevPageTxt, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(topScreen, lButtonIcon, GUI_ELEMENT_TYPE_IMAGE); + } + + if (nextPageEnabled) { + addToGuiScreen(topScreen, nextPageTxt, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(topScreen, rButtonIcon, GUI_ELEMENT_TYPE_IMAGE); + } + + addToGuiScreen(bottomScreen, bg, GUI_ELEMENT_TYPE_BOX); + + addNavbarToGuiScreen(bottomScreen); + + bool firstRun = true; + + size_t scrollOffset = 0; + size_t selectOffset = 0; + + bool selectUpAction = false; + bool selectDownAction = false; + bool scrollUpAction = false; + bool scrollDownAction = false; + bool scrollUpFastAction = false; + bool scrollDownFastAction = false; + + bool clickAction = false; + + bool nextPageAction = false; + bool prevPageAction = false; + + bool selectAction = false; + bool scrollAction = false; + bool switchPageAction = false; + + size_t displayedResultsCount = min(MAX_RESULTS_DISPLAY, resultsCount); + if (resultsCount > 0) + setGuiButtonState(resultsBtn[0], GUI_BUTTON_STATE_SELECTED); + + setActiveScreens(topScreen, bottomScreen); + while (pmMainLoop() && menu == MENU_NONE) { + guiLoop(); + + if (resultsCount > 0) { + selectUpAction = (pressed & KEY_UP); + selectDownAction = (pressed & KEY_DOWN); + scrollUpAction = (selectUpAction && getGuiButtonState(resultsBtn[scrollOffset]) == GUI_BUTTON_STATE_SELECTED); // Up is pressed and highest result is selected + scrollDownAction = (selectDownAction && getGuiButtonState(resultsBtn[scrollOffset + displayedResultsCount - 1]) == GUI_BUTTON_STATE_SELECTED); // Down is pressed and lowest result is selected + scrollUpFastAction = (pressed & KEY_LEFT); + scrollDownFastAction = (pressed & KEY_RIGHT); + clickAction = (pressed & KEY_A); + nextPageAction = (pressed & KEY_R); + prevPageAction = (pressed & KEY_L); + + selectAction = selectUpAction || selectDownAction; + scrollAction = scrollUpAction || scrollDownAction || scrollUpFastAction || scrollDownFastAction; + switchPageAction = nextPageAction || prevPageAction; + + if (scrollAction || firstRun) { + resetGuiButtonState(resultsBtn[scrollOffset + selectOffset]); + + if (!firstRun) { + // Remove all displayed results + for (size_t i = 0; i < displayedResultsCount; i++) { + removeFromGuiScreen(bottomScreen, resultsBtn[i + scrollOffset]); + removeFromGuiScreen(bottomScreen, resultsBtnTxtPlatform[i + scrollOffset]); + removeFromGuiScreen(bottomScreen, resultsBtnTxtRegion[i + scrollOffset]); + } + } else { + firstRun = false; + } + + if (scrollUpAction) { + if (scrollOffset > 0) + scrollOffset--; + + setGuiButtonState(resultsBtn[scrollOffset], GUI_BUTTON_STATE_SELECTED); + selectOffset = 0; + } else if (scrollDownAction) { + if (scrollOffset < resultsCount - 5) + scrollOffset++; + + setGuiButtonState(resultsBtn[scrollOffset + displayedResultsCount - 1], GUI_BUTTON_STATE_SELECTED); + selectOffset = displayedResultsCount - 1; + } else if (scrollUpFastAction) { + scrollOffset -= min(MAX_RESULTS_DISPLAY, scrollOffset); + + setGuiButtonState(resultsBtn[scrollOffset], GUI_BUTTON_STATE_SELECTED); + selectOffset = 0; + } else if (scrollDownFastAction) { + size_t maxPossibleStep = resultsCount - displayedResultsCount - scrollOffset; + if (maxPossibleStep > 0) + scrollOffset += min(MAX_RESULTS_DISPLAY, maxPossibleStep); + + setGuiButtonState(resultsBtn[scrollOffset], GUI_BUTTON_STATE_SELECTED); + selectOffset = 0; + } + + // Add displayed results according to scroll offset + for (size_t i = 0; i < displayedResultsCount; i++) { + setGuiButtonPos(resultsBtn[i + scrollOffset], 0, i * 32); + setGuiTextPos(resultsBtnTxtPlatform[i + scrollOffset], 246, i * 32 + 9); + setGuiTextPos(resultsBtnTxtRegion[i + scrollOffset], 246, i * 32 + 23); + + addToGuiScreen(bottomScreen, resultsBtn[i + scrollOffset], GUI_ELEMENT_TYPE_BUTTON); + addToGuiScreen(bottomScreen, resultsBtnTxtPlatform[i + scrollOffset], GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, resultsBtnTxtRegion[i + scrollOffset], GUI_ELEMENT_TYPE_TEXT); + } + } else if (selectAction) { + resetGuiButtonState(resultsBtn[scrollOffset + selectOffset]); + + if (selectUpAction && selectOffset > 0) + selectOffset--; + else if (selectDownAction && selectOffset < MAX_RESULTS_PER_PAGE - 1) + selectOffset++; + + setGuiButtonState(resultsBtn[scrollOffset + selectOffset], GUI_BUTTON_STATE_SELECTED); + } else if (clickAction) { + setGuiButtonState(resultsBtn[scrollOffset + selectOffset], GUI_BUTTON_STATE_CLICKED); + } else if (switchPageAction) { + // Set page and refresh menu + if (nextPageEnabled && nextPageAction) { + searchPage++; + switchMenu(MENU_RESULTS); + } else if (prevPageEnabled && prevPageAction) { + searchPage--; + switchMenu(MENU_RESULTS); + } + } else { + // Check for clicked result, set selected entry and switch to entry menu + for (size_t i = 0; i < displayedResultsCount; i++) { + if (getGuiButtonState(resultsBtn[i + scrollOffset]) != GUI_BUTTON_STATE_CLICKED) + continue; + + if (selectedEntry) + freeEntry(selectedEntry); + selectedEntry = cloneEntry(results[i + scrollOffset]); + switchMenu(MENU_ENTRY); + } + } + } + + drawScreens(); + + navbarSwitchMenu(); + } + + freeGuiScreen(topScreen); + freeGuiScreen(bottomScreen); + freeGuiBox(bg); + freeGuiImage(brickColor1); + freeGuiImage(brickColor2); + freeGuiText(menuTitle); + freeGuiText(prevPageTxt); + freeGuiText(nextPageTxt); + freeGuiImage(lButtonIcon); + freeGuiImage(rButtonIcon); + freeGuiBox(resultsBtnBg); + freeGuiBox(resultsBtnBgHover); + + for (size_t i = 0; i < resultsCount; i++) { + freeGuiText(resultsBtnTxt[i]); + freeGuiText(resultsBtnTxtPlatform[i]); + freeGuiText(resultsBtnTxtRegion[i]); + freeGuiButton(resultsBtn[i]); + freeEntry(results[i]); + } +} + +// Entry menu +// Displays the information of the selected entry +void entryMenu(void) +{ + menu = MENU_NONE; + + GuiScreen topScreen = newGuiScreen(GUI_SCREEN_LCD_TOP); + GuiScreen bottomScreen = newGuiScreen(GUI_SCREEN_LCD_BOTTOM); + + GuiBox topBg = newGuiBox(SCREEN_WIDTH, SCREEN_HEIGHT, col(COLOR_BG_2)); + GuiBox bottomBg = newGuiBox(SCREEN_WIDTH, SCREEN_HEIGHT, col(COLOR_BG)); + + GuiImage brickColor1 = NULL; + GuiImage brickColor2 = NULL; + + GuiText entryTitle = newGuiText(getEntryTitle(selectedEntry), GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT)); + setGuiTextPos(entryTitle, 128, 20); + setGuiTextAlignment(entryTitle, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + setGuiTextMaxWidth(entryTitle, 246); + setGuiTextWrap(entryTitle, true); + setGuiTextMaxHeight(entryTitle, 40); + + GuiText entryPlatformLabel = newGuiText(tr("Platform"), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT)); + setGuiTextPos(entryPlatformLabel, 80, 46); + setGuiTextAlignment(entryPlatformLabel, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + GuiText entryPlatform = newGuiText(getEntryPlatform(selectedEntry), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_2)); + setGuiTextPos(entryPlatform, 176, 46); + setGuiTextAlignment(entryPlatform, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + setGuiTextMaxWidth(entryPlatform, 96); + + GuiText entryRegionLabel = newGuiText(tr("Region"), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT)); + setGuiTextPos(entryRegionLabel, 80, 64); + setGuiTextAlignment(entryRegionLabel, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + GuiText entryRegion = newGuiText(getEntryRegion(selectedEntry), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_2)); + setGuiTextPos(entryRegion, 176, 64); + setGuiTextAlignment(entryRegion, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + setGuiTextMaxWidth(entryRegion, 96); + + GuiText entryAuthorLabel = newGuiText(tr("Author"), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT)); + setGuiTextPos(entryAuthorLabel, 80, 82); + setGuiTextAlignment(entryAuthorLabel, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + GuiText entryAuthor = newGuiText(getEntryAuthor(selectedEntry), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_2)); + setGuiTextPos(entryAuthor, 176, 82); + setGuiTextAlignment(entryAuthor, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + setGuiTextMaxWidth(entryAuthor, 96); + + GuiText entryVersionLabel = newGuiText(tr("Version"), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT)); + setGuiTextPos(entryVersionLabel, 80, 100); + setGuiTextAlignment(entryVersionLabel, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + GuiText entryVersion = newGuiText(getEntryVersion(selectedEntry), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_2)); + setGuiTextPos(entryVersion, 176, 100); + setGuiTextAlignment(entryVersion, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + setGuiTextMaxWidth(entryVersion, 96); + + char downloadBtnTxtText[32]; + u64 entrySize = getEntrySize(selectedEntry); + + if (entrySize) { + char sizeText[16]; + humanizeSize(sizeText, sizeof(sizeText), getEntrySize(selectedEntry)); + snprintf(downloadBtnTxtText, sizeof(downloadBtnTxtText), "%s (%s)", tr("Download"), sizeText); + } else { + snprintf(downloadBtnTxtText, sizeof(downloadBtnTxtText), "%s", tr("Download")); + } + + GuiText downloadBtnTxt = newGuiText(downloadBtnTxtText, GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT_2)); + setGuiTextAlignment(downloadBtnTxt, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + GuiBox downloadBtnBg = newGuiBox(getGuiTextWidth(downloadBtnTxt) + 20, 34, col(COLOR_BG_2)); + setGuiBoxBorder(downloadBtnBg, 2, col(COLOR_PRIMARY)); + GuiBox downloadBtnBgHover = newGuiBox(getGuiTextWidth(downloadBtnTxt) + 20, 34, col(COLOR_PRIMARY)); + + GuiButton downloadBtn = newGuiButton(getGuiTextWidth(downloadBtnTxt) + 20, 34); + setGuiButtonPos(downloadBtn, SCREEN_WIDTH / 2 - (getGuiTextWidth(downloadBtnTxt) + 20) / 2, 114); + setGuiButtonBg(downloadBtn, downloadBtnBg, downloadBtnBgHover); + setGuiButtonLabel(downloadBtn, downloadBtnTxt); + + GuiImage boxart = loadBoxart(selectedEntry); + + addToGuiScreen(topScreen, topBg, GUI_ELEMENT_TYPE_BOX); + + if (boxart) { + setGuiImagePos(boxart, 128, 96); + setGuiImageAlign(boxart, GUI_IMAGE_H_ALIGN_CENTER, GUI_IMAGE_V_ALIGN_MIDDLE); + + addToGuiScreen(topScreen, boxart, GUI_ELEMENT_TYPE_IMAGE); + } else { + brickColor1 = newGuiImage(brickColor1Bitmap, brickColor1Pal, 64, 48, 64, 64, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + brickColor2 = newGuiImage(brickColor2Bitmap, brickColor2Pal, 64, 48, 64, 64, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + setGuiImageColorTint(brickColor1, col(COLOR_PRIMARY)); + setGuiImageColorTint(brickColor2, col(COLOR_SECONDARY)); + setGuiImagePos(brickColor1, 96, 72); + setGuiImagePos(brickColor2, 96, 72); + + addToGuiScreen(topScreen, brickColor1, GUI_ELEMENT_TYPE_IMAGE); + addToGuiScreen(topScreen, brickColor2, GUI_ELEMENT_TYPE_IMAGE); + } + + addToGuiScreen(bottomScreen, bottomBg, GUI_ELEMENT_TYPE_BOX); + addToGuiScreen(bottomScreen, entryTitle, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, entryPlatformLabel, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, entryPlatform, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, entryRegionLabel, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, entryRegion, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, entryAuthorLabel, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, entryAuthor, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, entryVersionLabel, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, entryVersion, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, downloadBtn, GUI_ELEMENT_TYPE_BUTTON); + + addNavbarToGuiScreen(bottomScreen); + + setActiveScreens(topScreen, bottomScreen); + + while (pmMainLoop() && menu == MENU_NONE) { + guiLoop(); + drawScreens(); + + if (getGuiButtonState(downloadBtn) == GUI_BUTTON_STATE_CLICKED) + switchMenu(MENU_DOWNLOAD); + + navbarSwitchMenu(); + } + + freeGuiScreen(topScreen); + freeGuiScreen(bottomScreen); + freeGuiBox(topBg); + freeGuiBox(bottomBg); + freeGuiText(entryTitle); + freeGuiText(entryPlatformLabel); + freeGuiText(entryPlatform); + freeGuiText(entryRegionLabel); + freeGuiText(entryRegion); + freeGuiText(entryAuthorLabel); + freeGuiText(entryAuthor); + freeGuiText(entryVersionLabel); + freeGuiText(entryVersion); + freeGuiText(downloadBtnTxt); + freeGuiBox(downloadBtnBg); + freeGuiBox(downloadBtnBgHover); + freeGuiButton(downloadBtn); + + if (boxart) { + freeGuiImage(boxart); + } else { + freeGuiImage(brickColor1); + freeGuiImage(brickColor2); + } +} + +// Download menu +// Starts the download of the selected entry +void downloadMenu(void) +{ + downloadEntry(selectedEntry); + + switchMenu(MENU_BROWSE); +} + +#define MAX_DATABASES_DISPLAY 8 + +// Databases menu +// Displays the list of available databases +void databasesMenu(void) +{ + menu = MENU_NONE; + + GuiScreen topScreen = newGuiScreen(GUI_SCREEN_LCD_TOP); + GuiScreen bottomScreen = newGuiScreen(GUI_SCREEN_LCD_BOTTOM); + + GuiBox bg = newGuiBox(SCREEN_WIDTH, SCREEN_HEIGHT, col(COLOR_BG)); + + GuiImage brickColor1 = newGuiImage(brickColor1Bitmap, brickColor1Pal, 64, 48, 64, 64, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + GuiImage brickColor2 = newGuiImage(brickColor2Bitmap, brickColor2Pal, 64, 48, 64, 64, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + setGuiImageColorTint(brickColor1, col(COLOR_PRIMARY)); + setGuiImageColorTint(brickColor2, col(COLOR_SECONDARY)); + setGuiImagePos(brickColor1, 96, 72); + setGuiImagePos(brickColor2, 96, 72); + + GuiText menuTitle = newGuiText(tr("Databases"), GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT)); + setGuiTextPos(menuTitle, 128, 178); + setGuiTextAlignment(menuTitle, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + GuiText noDatabasesTxt = newGuiText(tr("No databases found"), GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT_3)); + setGuiTextPos(noDatabasesTxt, 128, 80); + setGuiTextAlignment(noDatabasesTxt, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + setGuiTextMaxWidth(noDatabasesTxt, 244); + setGuiTextWrap(noDatabasesTxt, true); + + GuiBox btnBg = newGuiBox(SCREEN_WIDTH, 20, col(COLOR_BG_2)); + setGuiBoxBorder(btnBg, 2, col(COLOR_PRIMARY)); + + GuiBox btnBgUsed = newGuiBox(SCREEN_WIDTH, 20, col(COLOR_SECONDARY)); + setGuiBoxBorder(btnBgUsed, 2, col(COLOR_PRIMARY)); + + GuiBox btnBgHover = newGuiBox(SCREEN_WIDTH, 20, col(COLOR_PRIMARY)); + + addToGuiScreen(topScreen, bg, GUI_ELEMENT_TYPE_BOX); + addToGuiScreen(topScreen, brickColor1, GUI_ELEMENT_TYPE_IMAGE); + addToGuiScreen(topScreen, brickColor2, GUI_ELEMENT_TYPE_IMAGE); + addToGuiScreen(topScreen, menuTitle, GUI_ELEMENT_TYPE_TEXT); + + addToGuiScreen(bottomScreen, bg, GUI_ELEMENT_TYPE_BOX); + + // Get database list and create screen elements + size_t dbCount; + Database* dbList = getDatabaseList(&dbCount); + + GuiText dbBtnTxtNames[dbCount]; + GuiText dbBtnTxtValues[dbCount]; + GuiButton dbBtns[dbCount]; + + for (size_t i = 0; i < dbCount && i < MAX_DATABASES_DISPLAY; i++) { + dbBtnTxtNames[i] = newGuiText(getDatabaseName(dbList[i]), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_2)); + setGuiTextPos(dbBtnTxtNames[i], 10, 0); + setGuiTextAlignment(dbBtnTxtNames[i], GUI_TEXT_H_ALIGN_LEFT, GUI_TEXT_V_ALIGN_MIDDLE); + setGuiTextMaxWidth(dbBtnTxtNames[i], 236); + + dbBtnTxtValues[i] = newGuiText(getDatabaseValue(dbList[i]), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_3)); + setGuiTextPos(dbBtnTxtValues[i], 246, i * 20 + 10); + setGuiTextAlignment(dbBtnTxtValues[i], GUI_TEXT_H_ALIGN_RIGHT, GUI_TEXT_V_ALIGN_MIDDLE); + setGuiTextMaxWidth(dbBtnTxtValues[i], 236 - getGuiTextWidth(dbBtnTxtNames[i])); + + dbBtns[i] = newGuiButton(SCREEN_WIDTH, 20); + + // Use different bg color if database is being used + if (db && strcmp(getDatabaseValue(db), getDatabaseValue(dbList[i])) == 0) + setGuiButtonBg(dbBtns[i], btnBgUsed, btnBgHover); + else + setGuiButtonBg(dbBtns[i], btnBg, btnBgHover); + + setGuiButtonLabel(dbBtns[i], dbBtnTxtNames[i]); + setGuiButtonPos(dbBtns[i], 0, i * 20); + + addToGuiScreen(bottomScreen, dbBtns[i], GUI_ELEMENT_TYPE_BUTTON); + addToGuiScreen(bottomScreen, dbBtnTxtValues[i], GUI_ELEMENT_TYPE_TEXT); + } + + if (dbCount == 0) + addToGuiScreen(bottomScreen, noDatabasesTxt, GUI_ELEMENT_TYPE_TEXT); + + addNavbarToGuiScreen(bottomScreen); + + setActiveScreens(topScreen, bottomScreen); + while (pmMainLoop() && menu == MENU_NONE) { + guiLoop(); + drawScreens(); + + for (size_t i = 0; i < dbCount; i++) { + if (getGuiButtonState(dbBtns[i]) != GUI_BUTTON_STATE_CLICKED) + continue; + + Database d = dbList[i]; + + if (!loadDatabase(d)) + freeDatabase(d); + + switchMenu(MENU_DATABASES); + } + + navbarSwitchMenu(); + } + + freeGuiScreen(topScreen); + freeGuiScreen(bottomScreen); + freeGuiBox(bg); + freeGuiImage(brickColor1); + freeGuiImage(brickColor2); + freeGuiText(menuTitle); + freeGuiText(noDatabasesTxt); + freeGuiBox(btnBg); + freeGuiBox(btnBgUsed); + freeGuiBox(btnBgHover); + + for (size_t i = 0; i < dbCount && i < MAX_DATABASES_DISPLAY; i++) { + freeGuiText(dbBtnTxtNames[i]); + freeGuiText(dbBtnTxtValues[i]); + freeGuiButton(dbBtns[i]); + + if (dbList[i] != db) + freeDatabase(dbList[i]); + } +} + +// Settings menu +// Displays the app settings +void settingsMenu(void) +{ + menu = MENU_NONE; + + GuiScreen topScreen = newGuiScreen(GUI_SCREEN_LCD_TOP); + GuiScreen bottomScreen = newGuiScreen(GUI_SCREEN_LCD_BOTTOM); + + GuiBox bg = newGuiBox(SCREEN_WIDTH, SCREEN_HEIGHT, col(COLOR_BG)); + + GuiImage brickColor1 = newGuiImage(brickColor1Bitmap, brickColor1Pal, 64, 48, 64, 64, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + GuiImage brickColor2 = newGuiImage(brickColor2Bitmap, brickColor2Pal, 64, 48, 64, 64, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + setGuiImageColorTint(brickColor1, col(COLOR_PRIMARY)); + setGuiImageColorTint(brickColor2, col(COLOR_SECONDARY)); + setGuiImagePos(brickColor1, 96, 72); + setGuiImagePos(brickColor2, 96, 72); + + GuiText menuTitle = newGuiText(tr("Settings"), GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT)); + setGuiTextPos(menuTitle, 128, 178); + setGuiTextAlignment(menuTitle, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + GuiBox btnBg = newGuiBox(SCREEN_WIDTH, 32, col(COLOR_BG_2)); + setGuiBoxBorder(btnBg, 2, col(COLOR_PRIMARY)); + + GuiBox btnBgHover = newGuiBox(SCREEN_WIDTH, 32, col(COLOR_PRIMARY)); + + GuiText settingBtnTxt[SETTINGS_COUNT]; + GuiButton settingBtn[SETTINGS_COUNT]; + GuiText settingValueTxt[SETTINGS_COUNT]; + + char colorSchemeStr[4]; + snprintf(colorSchemeStr, sizeof(colorSchemeStr), "%d", settings.colorScheme + 1); + + settingBtnTxt[0] = newGuiText(tr("Downloads directory"), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_2)); + settingValueTxt[0] = newGuiText(settings.dlPath, GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_3)); + setGuiTextMaxWidth(settingBtnTxt[0], 122); + + settingBtnTxt[1] = newGuiText(tr("Use platform-specific directories (E.g. nds, gba)"), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_2)); + settingValueTxt[1] = newGuiText(tr(dlUseDirsStr(settings.dlUseDirs)), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_3)); + setGuiTextMaxWidth(settingBtnTxt[1], 168); + + settingBtnTxt[2] = newGuiText(tr("Color scheme"), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_2)); + settingValueTxt[2] = newGuiText(colorSchemeStr, GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_3)); + setGuiTextMaxWidth(settingBtnTxt[2], 168); + + settingBtnTxt[3] = newGuiText(tr("Check for updates on start"), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_2)); + settingValueTxt[3] = newGuiText(tr(checkUpdateOnStartStr(settings.checkUpdateOnStart)), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_3)); + setGuiTextMaxWidth(settingBtnTxt[3], 168); + + settingBtnTxt[4] = newGuiText(tr("Language"), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_2)); + settingValueTxt[4] = newGuiText(tr(langStr(settings.lang)), GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_3)); + setGuiTextMaxWidth(settingBtnTxt[4], 168); + + addToGuiScreen(topScreen, bg, GUI_ELEMENT_TYPE_BOX); + addToGuiScreen(topScreen, brickColor1, GUI_ELEMENT_TYPE_IMAGE); + addToGuiScreen(topScreen, brickColor2, GUI_ELEMENT_TYPE_IMAGE); + addToGuiScreen(topScreen, menuTitle, GUI_ELEMENT_TYPE_TEXT); + + addToGuiScreen(bottomScreen, bg, GUI_ELEMENT_TYPE_BOX); + + for (size_t i = 0; i < SETTINGS_COUNT; i++) { + setGuiTextPos(settingBtnTxt[i], 12, 0); + setGuiTextAlignment(settingBtnTxt[i], GUI_TEXT_H_ALIGN_LEFT, GUI_TEXT_V_ALIGN_MIDDLE); + setGuiTextMaxHeight(settingBtnTxt[i], 32); + setGuiTextWrap(settingBtnTxt[i], true); + + setGuiTextPos(settingValueTxt[i], 244, i * 32 + 16); + setGuiTextAlignment(settingValueTxt[i], GUI_TEXT_H_ALIGN_RIGHT, GUI_TEXT_V_ALIGN_MIDDLE); + setGuiTextMaxWidth(settingValueTxt[i], 232 - getGuiTextWidth(settingBtnTxt[i])); + setGuiTextMaxHeight(settingValueTxt[i], 32); + setGuiTextWrap(settingValueTxt[i], true); + + settingBtn[i] = newGuiButton(SCREEN_WIDTH, 32); + setGuiButtonBg(settingBtn[i], btnBg, btnBgHover); + setGuiButtonLabel(settingBtn[i], settingBtnTxt[i]); + setGuiButtonPos(settingBtn[i], 0, i * 32); + + addToGuiScreen(bottomScreen, settingBtn[i], GUI_ELEMENT_TYPE_BUTTON); + addToGuiScreen(bottomScreen, settingValueTxt[i], GUI_ELEMENT_TYPE_TEXT); + } + + addNavbarToGuiScreen(bottomScreen); + + setActiveScreens(topScreen, bottomScreen); + while (pmMainLoop() && menu == MENU_NONE) { + guiLoop(); + drawScreens(); + + if (getGuiButtonState(settingBtn[0]) == GUI_BUTTON_STATE_CLICKED) { + char tempDlPath[PATH_MAX]; + strcpy(tempDlPath, settings.dlPath); + + if (keyboardEdit(tempDlPath, sizeof(tempDlPath))) { + bool save = true; + + if (tempDlPath[0] == '\0') { + windowPrompt(tr("Error"), tr("Path can not be empty"), tr("Ok"), NULL); + save = false; + } else if (!dirExists(tempDlPath)) { + if (windowPrompt(tr("Error"), tr("Directory does not exist. Do you want to create it?"), tr("Ok"), tr("Cancel"))) { + if (!createDirR(tempDlPath)) { + windowPrompt(tr("Error"), tr("Failed to create directory"), tr("Ok"), NULL); + save = false; + } + } else { + save = false; + } + } + + if (save) { + strcpy(settings.dlPath, tempDlPath); + saveSettings(); + } + } + + setGuiTextText(settingValueTxt[0], settings.dlPath); + + resetGuiButtonState(settingBtn[0]); + } else if (getGuiButtonState(settingBtn[1]) == GUI_BUTTON_STATE_CLICKED) { + settings.dlUseDirs = settings.dlUseDirs ? false : true; + + setGuiTextText(settingValueTxt[1], tr(dlUseDirsStr(settings.dlUseDirs))); + + saveSettings(); + resetGuiButtonState(settingBtn[1]); + } else if (getGuiButtonState(settingBtn[2]) == GUI_BUTTON_STATE_CLICKED) { + settings.colorScheme++; + + if (settings.colorScheme == COLOR_SCHEMES_COUNT) + settings.colorScheme = 0; + + snprintf(colorSchemeStr, sizeof(colorSchemeStr), "%d", settings.colorScheme + 1); + setGuiTextText(settingValueTxt[2], colorSchemeStr); + + freeNavbar(); + initNavbar(); + switchMenu(MENU_SETTINGS); + + saveSettings(); + resetGuiButtonState(settingBtn[2]); + } else if (getGuiButtonState(settingBtn[3]) == GUI_BUTTON_STATE_CLICKED) { + settings.checkUpdateOnStart = settings.checkUpdateOnStart ? false : true; + + setGuiTextText(settingValueTxt[3], tr(checkUpdateOnStartStr(settings.checkUpdateOnStart))); + + saveSettings(); + resetGuiButtonState(settingBtn[3]); + } else if (getGuiButtonState(settingBtn[4]) == GUI_BUTTON_STATE_CLICKED) { + settings.lang++; + + if (settings.lang == LANGS_COUNT) + settings.lang = 0; + + setGuiTextText(settingValueTxt[4], tr(langStr(settings.lang))); + + loadLanguage(settings.lang); + switchMenu(MENU_SETTINGS); + + saveSettings(); + resetGuiButtonState(settingBtn[4]); + } + + navbarSwitchMenu(); + } + + freeGuiScreen(topScreen); + freeGuiScreen(bottomScreen); + freeGuiBox(bg); + freeGuiImage(brickColor1); + freeGuiImage(brickColor2); + freeGuiText(menuTitle); + freeGuiBox(btnBg); + freeGuiBox(btnBgHover); + + for (size_t i = 0; i < SETTINGS_COUNT; i++) { + freeGuiText(settingBtnTxt[i]); + freeGuiText(settingValueTxt[i]); + freeGuiButton(settingBtn[i]); + } +} + +// Info menu +// Displays information about Kekatsu +void infoMenu(void) +{ + menu = MENU_NONE; + + GuiScreen topScreen = newGuiScreen(GUI_SCREEN_LCD_TOP); + GuiScreen bottomScreen = newGuiScreen(GUI_SCREEN_LCD_BOTTOM); + + GuiBox bg = newGuiBox(SCREEN_WIDTH, SCREEN_HEIGHT, col(COLOR_BG)); + + GuiImage brickColor1 = newGuiImage(brickColor1Bitmap, brickColor1Pal, 64, 48, 64, 64, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + GuiImage brickColor2 = newGuiImage(brickColor2Bitmap, brickColor2Pal, 64, 48, 64, 64, 0, 0, GUI_IMAGE_TEXTURE_TYPE_RGB256); + setGuiImageColorTint(brickColor1, col(COLOR_PRIMARY)); + setGuiImageColorTint(brickColor2, col(COLOR_SECONDARY)); + setGuiImagePos(brickColor1, 96, 72); + setGuiImagePos(brickColor2, 96, 72); + + GuiText menuTitle = newGuiText(tr("Info"), GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT)); + setGuiTextPos(menuTitle, 128, 178); + setGuiTextAlignment(menuTitle, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + GuiText appNameTxt = newGuiText(APP_NAME, GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT)); + setGuiTextPos(appNameTxt, 128, 22); + setGuiTextAlignment(appNameTxt, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + char appVersionText[16]; + snprintf(appVersionText, sizeof(appVersionText), "%s %s", tr("Version"), APP_VERSION); + + GuiText appVersionTxt = newGuiText(appVersionText, GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT_2)); + setGuiTextPos(appVersionTxt, 128, 43); + setGuiTextAlignment(appVersionTxt, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + GuiText subtitle1Txt = newGuiText("by Cavv", GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_2)); + setGuiTextPos(subtitle1Txt, 128, 72); + setGuiTextAlignment(subtitle1Txt, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + GuiText subtitle2Txt = newGuiText("https://github.com/cavv-dev/Kekatsu-DS", GUI_TEXT_SIZE_SMALL, col(COLOR_TEXT_2)); + setGuiTextPos(subtitle2Txt, 128, 87); + setGuiTextAlignment(subtitle2Txt, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + GuiText checkUpdateBtnTxt = newGuiText(tr("Check for updates"), GUI_TEXT_SIZE_MEDIUM, col(COLOR_TEXT_2)); + setGuiTextAlignment(checkUpdateBtnTxt, GUI_TEXT_H_ALIGN_CENTER, GUI_TEXT_V_ALIGN_MIDDLE); + + size_t checkUpdateBtnWidth = getGuiTextWidth(checkUpdateBtnTxt) + 20; + + GuiBox checkUpdateBtnBg = newGuiBox(checkUpdateBtnWidth, 34, col(COLOR_BG_2)); + setGuiBoxBorder(checkUpdateBtnBg, 2, col(COLOR_PRIMARY)); + GuiBox checkUpdateBtnBgHover = newGuiBox(checkUpdateBtnWidth, 32, col(COLOR_PRIMARY)); + + GuiButton checkUpdateBtn = newGuiButton(checkUpdateBtnWidth, 34); + setGuiButtonBg(checkUpdateBtn, checkUpdateBtnBg, checkUpdateBtnBgHover); + setGuiButtonLabel(checkUpdateBtn, checkUpdateBtnTxt); + setGuiButtonPos(checkUpdateBtn, 128 - checkUpdateBtnWidth / 2, 106); + + addToGuiScreen(topScreen, bg, GUI_ELEMENT_TYPE_BOX); + addToGuiScreen(topScreen, brickColor1, GUI_ELEMENT_TYPE_IMAGE); + addToGuiScreen(topScreen, brickColor2, GUI_ELEMENT_TYPE_IMAGE); + addToGuiScreen(topScreen, menuTitle, GUI_ELEMENT_TYPE_TEXT); + + addToGuiScreen(bottomScreen, bg, GUI_ELEMENT_TYPE_BOX); + addToGuiScreen(bottomScreen, appNameTxt, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, appVersionTxt, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, subtitle1Txt, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, subtitle2Txt, GUI_ELEMENT_TYPE_TEXT); + addToGuiScreen(bottomScreen, checkUpdateBtn, GUI_ELEMENT_TYPE_BUTTON); + + addNavbarToGuiScreen(bottomScreen); + + setActiveScreens(topScreen, bottomScreen); + while (pmMainLoop() && menu == MENU_NONE) { + guiLoop(); + drawScreens(); + + if (getGuiButtonState(checkUpdateBtn) == GUI_BUTTON_STATE_CLICKED) { + handleUpdateCheck(); + resetGuiButtonState(checkUpdateBtn); + } + + navbarSwitchMenu(); + } + + freeGuiScreen(topScreen); + freeGuiScreen(bottomScreen); + freeGuiBox(bg); + freeGuiImage(brickColor1); + freeGuiImage(brickColor2); + freeGuiText(menuTitle); + freeGuiText(appNameTxt); + freeGuiText(appVersionTxt); + freeGuiText(subtitle1Txt); + freeGuiText(subtitle2Txt); + freeGuiText(checkUpdateBtnTxt); + freeGuiBox(checkUpdateBtnBg); + freeGuiBox(checkUpdateBtnBgHover); + freeGuiButton(checkUpdateBtn); +} + +// Initializes the GUI elements and starts the menu system +void menuBegin(MenuEnum startingMenu) +{ + drawBlank(); + + loadLanguage(settings.lang); + initColorSchemes(); + initNavbar(); + + switchMenu(startingMenu); + + loadLastOpenedDb(); + + if (settings.checkUpdateOnStart) + handleUpdateCheck(); + + while (menu != MENU_EXIT) { + switch (menu) { + case MENU_BROWSE: + browseMenu(); + break; + case MENU_RESULTS: + resultsMenu(); + break; + case MENU_ENTRY: + entryMenu(); + break; + case MENU_DOWNLOAD: + downloadMenu(); + break; + case MENU_DATABASES: + databasesMenu(); + break; + case MENU_SETTINGS: + settingsMenu(); + break; + case MENU_INFO: + infoMenu(); + break; + default: + break; + } + } + + drawBlank(); +} diff --git a/source/menu.h b/source/menu.h new file mode 100644 index 0000000..5d995c0 --- /dev/null +++ b/source/menu.h @@ -0,0 +1,15 @@ +#pragma once + +typedef enum { + MENU_NONE, + MENU_BROWSE, + MENU_RESULTS, + MENU_ENTRY, + MENU_DOWNLOAD, + MENU_DATABASES, + MENU_SETTINGS, + MENU_INFO, + MENU_EXIT +} MenuEnum; + +void menuBegin(MenuEnum); diff --git a/source/networking.c b/source/networking.c new file mode 100644 index 0000000..0b7455e --- /dev/null +++ b/source/networking.c @@ -0,0 +1,179 @@ +#include "networking.h" + +#include "config.h" +#include +#include +#include +#include + +#define BUFFER_SIZE 1024 * 1024 // 1 MiB +#define USER_AGENT APP_NAME "/" APP_VERSION " (DS)" + +NetworkingInitStatus initNetworking(void) +{ + if (!Wifi_InitDefault(WFC_CONNECT)) + return NETWORKING_INIT_ERR_WIFI_CONNECT; + + curl_global_init(CURL_GLOBAL_DEFAULT); + + return NETWORKING_INIT_SUCCESS; +} + +bool stopDownloadSignal = false; + +struct WriteData { + FILE* fp; + char* buffer; + size_t bufferPos; +}; + +size_t writeDataCallback(void* ptr, size_t size, size_t nmemb, void* userdata) +{ + struct WriteData* writeData = (struct WriteData*)userdata; + if (stopDownloadSignal) + return -1; + + size_t total_size = size * nmemb; + if (writeData->bufferPos + total_size > BUFFER_SIZE) { + fwrite(writeData->buffer, 1, writeData->bufferPos, writeData->fp); + writeData->bufferPos = 0; + } + + if (total_size > BUFFER_SIZE) { + fwrite(ptr, 1, total_size, writeData->fp); + } else { + memcpy(writeData->buffer + writeData->bufferPos, ptr, total_size); + writeData->bufferPos += total_size; + } + + return total_size; +} + +DownloadStatus downloadFile(const char* path, const char* url, size_t (*downloadProgressCallback)(void*, curl_off_t, curl_off_t, curl_off_t, curl_off_t)) +{ + stopDownloadSignal = false; + + CURL* curl = curl_easy_init(); + if (!curl) + return DOWNLOAD_ERR_INIT_FAILED; + + FILE* fp = fopen(path, "wb"); + if (!fp) { + curl_easy_cleanup(curl); + return DOWNLOAD_ERR_FILE_OPEN_FAILED; + } + + struct WriteData writeData; + writeData.fp = fp; + writeData.buffer = (char*)malloc(BUFFER_SIZE); + writeData.bufferPos = 0; + + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_USERAGENT, USER_AGENT); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeDataCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &writeData); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 7L); + + if (downloadProgressCallback) { + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); + curl_easy_setopt(curl, CURLOPT_XFERINFODATA, NULL); + curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, downloadProgressCallback); + } + + CURLcode res = curl_easy_perform(curl); + + if (writeData.bufferPos > 0) + fwrite(writeData.buffer, 1, writeData.bufferPos, writeData.fp); + + free(writeData.buffer); + fclose(fp); + + if (stopDownloadSignal) { + curl_easy_cleanup(curl); + unlink(path); + return DOWNLOAD_STOPPED; + } + + if (res != CURLE_OK) { + curl_easy_cleanup(curl); + unlink(path); + return DOWNLOAD_ERR_PERFORM_FAILED; + } + + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + curl_easy_cleanup(curl); + + if (httpCode != 200) { + unlink(path); + return DOWNLOAD_ERR_NOT_OK; + } + + return DOWNLOAD_SUCCESS; +} + +void stopDownload(void) +{ + stopDownloadSignal = true; +} + +struct MemoryWriteData { + char* result; + size_t bufferSize; + size_t currentPos; +}; + +size_t memoryWriteCallback(void* ptr, size_t size, size_t nmemb, void* userdata) +{ + struct MemoryWriteData* writeData = (struct MemoryWriteData*)userdata; + size_t total_size = size * nmemb; + + if (writeData->currentPos + total_size >= writeData->bufferSize) { + total_size = writeData->bufferSize - writeData->currentPos - 1; + } + + memcpy(writeData->result + writeData->currentPos, ptr, total_size); + writeData->currentPos += total_size; + writeData->result[writeData->currentPos] = '\0'; + + return total_size; +} + +DownloadStatus downloadToString(char* result, size_t bufferSize, const char* url) +{ + CURL* curl = curl_easy_init(); + if (!curl) + return DOWNLOAD_ERR_INIT_FAILED; + + struct MemoryWriteData writeData; + writeData.result = result; + writeData.bufferSize = bufferSize; + writeData.currentPos = 0; + result[0] = '\0'; + + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_USERAGENT, USER_AGENT); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, memoryWriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &writeData); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 7L); + + CURLcode res = curl_easy_perform(curl); + + if (res != CURLE_OK) { + curl_easy_cleanup(curl); + return DOWNLOAD_ERR_PERFORM_FAILED; + } + + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + curl_easy_cleanup(curl); + + if (httpCode != 200) + return DOWNLOAD_ERR_NOT_OK; + + return DOWNLOAD_SUCCESS; +} diff --git a/source/networking.h b/source/networking.h new file mode 100644 index 0000000..05fa7ac --- /dev/null +++ b/source/networking.h @@ -0,0 +1,21 @@ +#pragma once +#include + +typedef enum { + NETWORKING_INIT_SUCCESS, + NETWORKING_INIT_ERR_WIFI_CONNECT +} NetworkingInitStatus; + +typedef enum { + DOWNLOAD_SUCCESS, + DOWNLOAD_STOPPED, + DOWNLOAD_ERR_NOT_OK, + DOWNLOAD_ERR_INIT_FAILED, + DOWNLOAD_ERR_FILE_OPEN_FAILED, + DOWNLOAD_ERR_PERFORM_FAILED +} DownloadStatus; + +NetworkingInitStatus initNetworking(void); +DownloadStatus downloadFile(const char* path, const char* url, size_t (*downloadProgressCallback)(void*, curl_off_t, curl_off_t, curl_off_t, curl_off_t)); +void stopDownload(void); +DownloadStatus downloadToString(char* result, size_t bufferSize, const char* url); diff --git a/source/settings.c b/source/settings.c new file mode 100644 index 0000000..7d25604 --- /dev/null +++ b/source/settings.c @@ -0,0 +1,80 @@ +#include "settings.h" + +#include "config.h" +#include "utils/filesystem.h" +#include +#include +#include + +#define SETTINGS_FILENAME "settings.cfg" + +struct Settings settings; + +bool defaultSettings(void) +{ + if (dirExists("/roms")) { + sprintf(settings.dlPath, "/roms"); + settings.dlUseDirs = true; // The user probably wants to use platform-specific directories if has a roms directory + } else { + sprintf(settings.dlPath, "/"); + settings.dlUseDirs = false; + } + + settings.colorScheme = COLOR_SCHEME_1; + settings.lang = LANG_EN; + settings.checkUpdateOnStart = true; + + return true; +} + +bool loadSettings(void) +{ + FILE* fp = fopen(APPDATA_DIR "/" SETTINGS_FILENAME, "r"); + if (!fp) + return false; + + char line[2048]; + char name[1024]; + char value[1024]; + while (fgets(line, sizeof(line), fp)) { + line[strcspn(line, "\r\n")] = 0; + + if (sscanf(line, "%[^=]=%s", name, value) != 2) { + fclose(fp); + return false; + } + + int valueInt = atoi(value); + + if (strcmp(name, "dlPath") == 0 && dirExists(value)) + snprintf(settings.dlPath, sizeof(settings.dlPath), value); + else if (strcmp(name, "useDirs") == 0 && (valueInt == 0 || valueInt == 1)) + settings.dlUseDirs = valueInt; + else if (strcmp(name, "colorScheme") == 0 && (valueInt > 0 && valueInt < COLOR_SCHEMES_COUNT + 1)) + settings.colorScheme = valueInt - 1; + else if (strcmp(name, "lang") == 0 && (valueInt >= 0 && valueInt < LANGS_COUNT)) + settings.lang = valueInt; + else if (strcmp(name, "checkUpdateOnStart") == 0 && (valueInt == 0 || valueInt == 1)) + settings.checkUpdateOnStart = valueInt; + } + + fclose(fp); + + return true; +} + +bool saveSettings(void) +{ + FILE* fp = fopen(APPDATA_DIR "/" SETTINGS_FILENAME, "w"); + if (!fp) + return false; + + fprintf(fp, "dlPath=%s\n", settings.dlPath); + fprintf(fp, "useDirs=%d\n", settings.dlUseDirs); + fprintf(fp, "colorScheme=%d\n", settings.colorScheme + 1); + fprintf(fp, "lang=%d\n", settings.lang); + fprintf(fp, "checkUpdateOnStart=%d\n", settings.checkUpdateOnStart); + + fclose(fp); + return true; +} diff --git a/source/settings.h b/source/settings.h new file mode 100644 index 0000000..eafb127 --- /dev/null +++ b/source/settings.h @@ -0,0 +1,42 @@ +#pragma once +#include "colors.h" +#include "gettext.h" +#include + +typedef enum { + SETTING_DLPATH, + SETTING_DLUSEDIRS, + SETTING_COLORSCHEME, + SETTING_CHECKUPDATEONSTART, + SETTING_LANG, + SETTINGS_COUNT +} SettingEnum; + +struct Settings { + char dlPath[PATH_MAX]; + bool dlUseDirs; + ColorSchemeEnum colorScheme; + LanguageEnum lang; + bool checkUpdateOnStart; +}; + +extern struct Settings settings; + +#define dlUseDirsStr(x) \ + ((x) == false ? "No" \ + : (x) == true ? "Yes" \ + : "") + +#define langStr(x) \ + ((x) == LANG_EN ? "English" \ + : (x) == LANG_IT ? "Italian" \ + : "") + +#define checkUpdateOnStartStr(x) \ + ((x) == false ? "No" \ + : (x) == true ? "Yes" \ + : "") + +bool defaultSettings(void); +bool loadSettings(void); +bool saveSettings(void); diff --git a/source/utils/filesystem.c b/source/utils/filesystem.c new file mode 100644 index 0000000..fc7053d --- /dev/null +++ b/source/utils/filesystem.c @@ -0,0 +1,164 @@ +#include "filesystem.h" + +#include +#include +#include +#include +#include +#include + +bool fileExists(const char* filePath) +{ + struct stat statbuf; + if (stat(filePath, &statbuf) == 0 && S_ISREG(statbuf.st_mode)) + return true; + else + return false; +} + +bool dirExists(const char* dirPath) +{ + struct stat statbuf; + if (stat(dirPath, &statbuf) == 0 && S_ISDIR(statbuf.st_mode)) + return true; + else + return false; +} + +bool pathExists(const char* path) +{ + return fileExists(path) || dirExists(path); +} + +bool deleteFile(const char* filePath) +{ + if (unlink(filePath) != 0) + return false; + return true; +} + +bool deleteDir(const char* dirPath) +{ + DIR* dp = opendir(dirPath); + if (dp == NULL) + return false; + + struct dirent* entry; + while ((entry = readdir(dp)) != NULL) { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) + continue; + + char fullPath[PATH_MAX]; + snprintf(fullPath, sizeof(fullPath), "%s/%s", dirPath, entry->d_name); + + struct stat statbuf; + if (stat(fullPath, &statbuf) != 0) { + closedir(dp); + return false; + } + + if (S_ISDIR(statbuf.st_mode)) { + if (!deleteDir(fullPath)) { + closedir(dp); + return false; + } + } else { + if (!deleteFile(fullPath)) { + closedir(dp); + return false; + } + } + } + + closedir(dp); + + if (rmdir(dirPath) != 0) + return false; + + return true; +} + +bool deletePath(const char* path) +{ + return deleteFile(path) || deleteDir(path); +} + +bool renamePath(const char* oldPath, const char* newPath) +{ + if (pathExists(newPath)) { + if (!deletePath(newPath)) + return false; + } + + if (rename(oldPath, newPath) != 0) + return false; + + return true; +} + +bool createDir(const char* dirPath) +{ + if (mkdir(dirPath, 0755) == 0 || errno == EEXIST) + return true; + else + return false; +} + +bool createDirR(const char* dirPath) +{ + char tempPath[PATH_MAX]; + char* p = NULL; + size_t len; + + strncpy(tempPath, dirPath, sizeof(tempPath) - 1); + len = strlen(tempPath); + + // Remove trailing slash if present + if (tempPath[len - 1] == '/') + tempPath[len - 1] = '\0'; + + // Skip device name if present (e.g., "fat:") + p = strchr(tempPath, ':'); + if (p != NULL) + p++; // Move past the ':' + else + p = tempPath; + + // Iterate through the path and create directories as needed + for (; *p; p++) { + if (*p != '/') + continue; + + *p = '\0'; + + if (!createDir(tempPath)) + return false; + + *p = '/'; + } + + // Create the final directory + return createDir(tempPath); +} + +void getPathDir(const char* path, char* dir) +{ + const char* lastSlash = strrchr(path, '/'); + + if (!lastSlash) { + dir[0] = '\0'; + return; + } + + size_t dirLen = lastSlash - path + 1; + strncpy(dir, path, dirLen); + dir[dirLen] = '\0'; +} + +bool createDirStructure(const char* path) +{ + char dirPath[PATH_MAX]; + getPathDir(path, dirPath); + + return createDirR(dirPath); +} diff --git a/source/utils/filesystem.h b/source/utils/filesystem.h new file mode 100644 index 0000000..b8a6866 --- /dev/null +++ b/source/utils/filesystem.h @@ -0,0 +1,13 @@ +#pragma once +#include + +bool fileExists(const char* filePath); +bool dirExists(const char* dirPath); +bool pathExists(const char* path); +bool deleteFile(const char* filePath); +bool deleteDir(const char* dirPath); +bool deletePath(const char* path); +bool renamePath(const char* oldPath, const char* newPath); +bool createDir(const char* dirPath); +bool createDirR(const char* dirPath); +bool createDirStructure(const char* path); diff --git a/source/utils/math.h b/source/utils/math.h new file mode 100644 index 0000000..0030795 --- /dev/null +++ b/source/utils/math.h @@ -0,0 +1,4 @@ +#pragma once + +#define max(a, b) (((a) > (b)) ? (a) : (b)) +#define min(a, b) (((a) < (b)) ? (a) : (b)) diff --git a/source/utils/strings.c b/source/utils/strings.c new file mode 100644 index 0000000..c6ac466 --- /dev/null +++ b/source/utils/strings.c @@ -0,0 +1,76 @@ +#include "strings.h" + +#include +#include +#include + +void humanizeSize(char* sizeStr, size_t bufferSize, u64 sizeInBytes) +{ + const char* suffix[] = { "B", "KiB", "MiB", "GiB" }; + u8 suffixesCount = 4; + + u8 i = 0; + double dblBytes = sizeInBytes; + if (sizeInBytes > 1024) { + for (i = 0; (sizeInBytes / 1024) > 0 && i < suffixesCount - 1; i++, sizeInBytes /= 1024) + dblBytes = (double)sizeInBytes / 1024; + } + + snprintf(sizeStr, bufferSize, "%.01lf %s", dblBytes, suffix[i]); +} + +void lowerStr(char* str) +{ + for (size_t i = 0; str[i] != '\0'; i++) + str[i] = tolower(str[i]); +} + +void removeAccentsStr(char* str) +{ + const char normalizedCharMap[] = { + (char)0, (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, (char)31, (char)32, (char)33, (char)34, (char)35, (char)36, (char)37, (char)38, (char)39, (char)40, (char)41, (char)42, (char)43, (char)44, (char)45, (char)46, (char)47, (char)48, (char)49, (char)50, (char)51, (char)52, (char)53, (char)54, (char)55, (char)56, (char)57, (char)58, (char)59, (char)60, (char)61, (char)62, (char)63, (char)64, (char)65, (char)66, (char)67, (char)68, (char)69, (char)70, (char)71, (char)72, (char)73, (char)74, (char)75, (char)76, (char)77, (char)78, (char)79, (char)80, (char)81, (char)82, (char)83, (char)84, (char)85, (char)86, (char)87, (char)88, (char)89, (char)90, (char)91, (char)92, (char)93, (char)94, (char)95, (char)96, (char)97, (char)98, (char)99, (char)100, (char)101, (char)102, (char)103, (char)104, (char)105, (char)106, (char)107, (char)108, (char)109, (char)110, (char)111, (char)112, (char)113, (char)114, (char)115, (char)116, (char)117, (char)118, (char)119, (char)120, (char)121, (char)122, (char)123, (char)124, (char)125, (char)126, (char)127, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)63, (char)32, (char)33, (char)63, (char)63, (char)63, (char)63, (char)124, (char)63, (char)34, (char)63, (char)97, (char)63, (char)33, (char)63, (char)63, (char)45, (char)63, (char)63, (char)50, (char)51, (char)39, (char)117, (char)80, (char)42, (char)44, (char)49, (char)111, (char)63, (char)63, (char)63, (char)63, (char)63, (char)65, (char)65, (char)65, (char)65, (char)65, (char)65, (char)63, (char)67, (char)69, (char)69, (char)69, (char)69, (char)73, (char)73, (char)73, (char)73, (char)68, (char)78, (char)79, (char)79, (char)79, (char)79, (char)79, (char)120, (char)79, (char)85, (char)85, (char)85, (char)85, (char)89, (char)63, (char)63, (char)97, (char)97, (char)97, (char)97, (char)97, (char)97, (char)63, (char)99, (char)101, (char)101, (char)101, (char)101, (char)105, (char)105, (char)105, (char)105, (char)100, (char)110, (char)111, (char)111, (char)111, (char)111, (char)111, (char)47, (char)111, (char)117, (char)117, (char)117, (char)117, (char)121, (char)63, (char)121 + }; + + // Replace each non-ASCII character with its corresponding in map + size_t i = 0, j = 0; + while (str[i] != '\0') { + u8 c = (u8)str[i]; + if (c < 128) + str[j++] = str[i]; + else + str[j++] = normalizedCharMap[c]; + + i++; + } + + str[j] = '\0'; +} + +void safeStr(char* str) +{ + removeAccentsStr(str); + + // Replace characters not in 'a-zA-Z0-9 ' with '_' + size_t i, j; + for (i = 0, j = 0; str[i]; i++) { + if ((str[i] >= 'a' && str[i] <= 'z') || (str[i] >= 'A' && str[i] <= 'Z') || (str[i] >= '0' && str[i] <= '9') || (str[i] == ' ')) + str[j++] = str[i]; + else + str[j++] = '_'; + } + + str[j] = '\0'; +} + +void joinPath(char* joinedPath, const char* path1, const char* path2) +{ + strcpy(joinedPath, path1); + + if (joinedPath[strlen(joinedPath) - 1] != '/') + strcat(joinedPath, "/"); + + if (path2[0] == '/') + strcat(joinedPath, path2 + 1); + else + strcat(joinedPath, path2); +} diff --git a/source/utils/strings.h b/source/utils/strings.h new file mode 100644 index 0000000..a31b777 --- /dev/null +++ b/source/utils/strings.h @@ -0,0 +1,8 @@ +#pragma once +#include + +void humanizeSize(char* sizeStr, size_t bufferSize, u64 sizeInBytes); +void lowerStr(char*); +void removeAccentsStr(char*); +void safeStr(char*); +void joinPath(char* joinedPath, const char* path1, const char* path2);