diff --git a/Makefile b/Makefile index b4ca3df..87d6303 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,8 @@ endif #--------------------------------------------------------------------------------- # BUILD_FLAGS: List of extra build flags to add. -# NO_CTRCOMMON: Do not include ctrcommon. # ENABLE_EXCEPTIONS: Enable C++ exceptions. +# NO_CITRUS: Do not include citrus. #--------------------------------------------------------------------------------- -include $(DEVKITPRO)/ctrcommon/tools/make_base \ No newline at end of file +include $(DEVKITPRO)/citrus/tools/make_base \ No newline at end of file diff --git a/README.md b/README.md index f006eb0..6b2cee4 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,4 @@ FBI is an open source CIA (un)installer for the 3DS. Download: https://github.com/Steveice10/FBI/releases -Requires [devkitARM](http://sourceforge.net/projects/devkitpro/files/devkitARM/) and [ctrcommon](https://github.com/Steveice10/ctrcommon) to build. +Requires [devkitARM](http://sourceforge.net/projects/devkitpro/files/devkitARM/) and [citrus](https://github.com/Steveice10/citrus) to build. diff --git a/source/main.cpp b/source/main.cpp index a7e8e97..d011e62 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -1,12 +1,16 @@ -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "rop.h" +#include "ui.hpp" #include #include @@ -14,6 +18,9 @@ #include #include +#include + +using namespace ctr; typedef enum { INSTALL_CIA, @@ -27,7 +34,7 @@ std::vector extensions = {"cia"}; bool exit = false; bool showNetworkPrompts = true; u64 freeSpace = 0; -MediaType destination = SD; +fs::MediaType destination = fs::SD; Mode mode = INSTALL_CIA; int prevProgress = -1; @@ -42,19 +49,17 @@ bool onProgress(u64 pos, u64 totalSize) { details << installInfo; details << "Press B to cancel."; - uiDisplayProgress(TOP_SCREEN, "Installing", details.str(), true, progress); + uiDisplayProgress(gpu::SCREEN_TOP, "Installing", details.str(), true, progress); } - inputPoll(); - return !inputIsPressed(BUTTON_B); + hid::poll(); + return !hid::pressed(hid::BUTTON_B); } void networkInstall() { - while(platformIsRunning()) { - gpuClearScreens(); - - RemoteFile file = uiAcceptRemoteFile(TOP_SCREEN, [&](std::stringstream& infoStream) { - if(inputIsPressed(BUTTON_A)) { + while(core::running()) { + RemoteFile file = uiAcceptRemoteFile(gpu::SCREEN_TOP, [&](std::stringstream& infoStream) { + if(hid::pressed(hid::BUTTON_A)) { showNetworkPrompts = !showNetworkPrompts; } @@ -70,22 +75,22 @@ void networkInstall() { std::stringstream confirmStream; confirmStream << "Install the received application?" << "\n"; confirmStream << "Size: " << file.fileSize << " bytes (" << std::fixed << std::setprecision(2) << file.fileSize / 1024.0f / 1024.0f << "MB)"; - if(!showNetworkPrompts || uiPrompt(TOP_SCREEN, confirmStream.str(), true)) { - AppResult ret = appInstall(destination, file.fd, file.fileSize, &onProgress); + if(!showNetworkPrompts || uiPrompt(gpu::SCREEN_TOP, confirmStream.str(), true)) { + app::AppResult ret = app::install(destination, file.fd, file.fileSize, &onProgress); prevProgress = -1; - if(showNetworkPrompts || ret != APP_SUCCESS) { + if(showNetworkPrompts || ret != app::APP_SUCCESS) { std::stringstream resultMsg; resultMsg << "Install "; - if(ret == APP_SUCCESS) { + if(ret == app::APP_SUCCESS) { resultMsg << "succeeded!"; } else { resultMsg << "failed!" << "\n"; - resultMsg << appGetResultString(ret); + resultMsg << app::resultString(ret); } - uiPrompt(TOP_SCREEN, resultMsg.str(), false); + uiPrompt(gpu::SCREEN_TOP, resultMsg.str(), false); - freeSpace = fsGetFreeSpace(destination); + freeSpace = fs::freeSpace(destination); } } @@ -96,20 +101,20 @@ void networkInstall() { void installROP() { u32 selected = 0; bool dirty = true; - while(platformIsRunning()) { - inputPoll(); - if(inputIsPressed(BUTTON_B)) { + while(core::running()) { + hid::poll(); + if(hid::pressed(hid::BUTTON_B)) { break; } - if(inputIsPressed(BUTTON_A)) { + if(hid::pressed(hid::BUTTON_A)) { std::stringstream stream; stream << "Install the selected ROP?" << "\n"; stream << ropNames[selected]; - if(uiPrompt(TOP_SCREEN, stream.str(), true)) { + if(uiPrompt(gpu::SCREEN_TOP, stream.str(), true)) { u16 userSettingsOffset = 0; - bool result = norRead(0x20, &userSettingsOffset, 2) && norWrite(userSettingsOffset << 3, rops[selected], ROP_SIZE); + bool result = nor::read(0x20, &userSettingsOffset, 2) && nor::write(userSettingsOffset << 3, rops[selected], ROP_SIZE); std::stringstream resultMsg; resultMsg << "ROP installation "; @@ -117,16 +122,16 @@ void installROP() { resultMsg << "succeeded!"; } else { resultMsg << "failed!" << "\n"; - resultMsg << platformGetErrorString(platformGetError()); + resultMsg << err::toString(err::get()); } - uiPrompt(TOP_SCREEN, resultMsg.str(), false); + uiPrompt(gpu::SCREEN_TOP, resultMsg.str(), false); } dirty = true; } - if(inputIsPressed(BUTTON_LEFT)) { + if(hid::pressed(hid::BUTTON_LEFT)) { if(selected == 0) { selected = ROP_COUNT - 1; } else { @@ -136,7 +141,7 @@ void installROP() { dirty = true; } - if(inputIsPressed(BUTTON_RIGHT)) { + if(hid::pressed(hid::BUTTON_RIGHT)) { if(selected >= ROP_COUNT - 1) { selected = 0; } else { @@ -151,62 +156,81 @@ void installROP() { stream << "Select a ROP to install." << "\n"; stream << "< " << ropNames[selected] << " >" << "\n"; stream << "Press A to install, B to cancel."; - uiDisplayMessage(TOP_SCREEN, stream.str()); + uiDisplayMessage(gpu::SCREEN_TOP, stream.str()); } } } -bool installCIA(MediaType destination, const std::string path, const std::string fileName) { +bool installCIA(fs::MediaType destination, const std::string path, const std::string fileName) { std::string name = fileName; if(name.length() > 40) { name.resize(40); name += "..."; } + FILE* fd = fopen(path.c_str(), "r"); + if(!fd) { + std::stringstream resultMsg; + resultMsg << "Install failed!" << "\n"; + resultMsg << name << "\n"; + resultMsg << "Could not open file." << "\n"; + resultMsg << strerror(errno) << "\n"; + uiPrompt(gpu::SCREEN_TOP, resultMsg.str(), false); + + return false; + } + + struct stat st; + fstat(fileno(fd), &st); + std::stringstream batchInstallStream; batchInstallStream << name << "\n"; installInfo = batchInstallStream.str(); - AppResult ret = appInstallFile(destination, path, &onProgress); + app::AppResult ret = app::install(destination, fd, (u64) st.st_size, &onProgress); prevProgress = -1; installInfo = ""; - if(ret != APP_SUCCESS && platformHasError()) { - Error error = platformGetError(); - if(error.module == MODULE_NN_AM && error.description == DESCRIPTION_ALREADY_EXISTS) { + if(ret != app::APP_SUCCESS && err::has()) { + err::Error error = err::get(); + if(error.module == err::MODULE_NN_AM && error.description == err::DESCRIPTION_ALREADY_EXISTS) { std::stringstream overwriteMsg; overwriteMsg << "Title already installed, overwrite?" << "\n"; overwriteMsg << name; - if(uiPrompt(TOP_SCREEN, overwriteMsg.str(), true)) { - uiDisplayMessage(TOP_SCREEN, "Deleting title..."); - AppResult deleteRet = appDelete(appGetCiaInfo(path, destination)); - if(deleteRet != APP_SUCCESS) { + if(uiPrompt(gpu::SCREEN_TOP, overwriteMsg.str(), true)) { + uiDisplayMessage(gpu::SCREEN_TOP, "Deleting title..."); + + app::App app; + app::ciaInfo(&app, path, destination); + + app::AppResult deleteRet = app::uninstall(app); + if(deleteRet != app::APP_SUCCESS) { std::stringstream resultMsg; resultMsg << "Delete failed!" << "\n"; resultMsg << name << "\n"; - resultMsg << appGetResultString(deleteRet); - uiPrompt(TOP_SCREEN, resultMsg.str(), false); + resultMsg << app::resultString(deleteRet); + uiPrompt(gpu::SCREEN_TOP, resultMsg.str(), false); return false; } - ret = appInstallFile(destination, path, &onProgress); + ret = app::install(destination, fd, (u64) st.st_size, &onProgress); prevProgress = -1; installInfo = ""; } else { - platformSetError(error); + err::set(error); } } else { - platformSetError(error); + err::set(error); } } - if(ret != APP_SUCCESS) { + if(ret != app::APP_SUCCESS) { std::stringstream resultMsg; resultMsg << "Install failed!" << "\n"; resultMsg << name << "\n"; - resultMsg << appGetResultString(ret); - uiPrompt(TOP_SCREEN, resultMsg.str(), false); + resultMsg << app::resultString(ret); + uiPrompt(gpu::SCREEN_TOP, resultMsg.str(), false); return false; } @@ -224,14 +248,14 @@ bool deleteCIA(const std::string path, const std::string fileName) { std::stringstream deleteStream; deleteStream << "Deleting CIA..." << "\n"; deleteStream << name; - uiDisplayMessage(TOP_SCREEN, deleteStream.str()); + uiDisplayMessage(gpu::SCREEN_TOP, deleteStream.str()); if(remove(path.c_str()) != 0) { std::stringstream resultMsg; resultMsg << "Delete failed!" << "\n"; resultMsg << name << "\n"; resultMsg << strerror(errno); - uiPrompt(TOP_SCREEN, resultMsg.str(), false); + uiPrompt(gpu::SCREEN_TOP, resultMsg.str(), false); return false; } @@ -239,15 +263,15 @@ bool deleteCIA(const std::string path, const std::string fileName) { return true; } -bool deleteTitle(App app) { - uiDisplayMessage(TOP_SCREEN, "Deleting title..."); - AppResult ret = appDelete(app); +bool deleteTitle(app::App app) { + uiDisplayMessage(gpu::SCREEN_TOP, "Deleting title..."); + app::AppResult ret = app::uninstall(app); - if(ret != APP_SUCCESS) { + if(ret != app::APP_SUCCESS) { std::stringstream resultMsg; resultMsg << "Delete failed!" << "\n"; - resultMsg << appGetResultString(ret); - uiPrompt(TOP_SCREEN, resultMsg.str(), false); + resultMsg << app::resultString(ret); + uiPrompt(gpu::SCREEN_TOP, resultMsg.str(), false); return false; } @@ -255,15 +279,15 @@ bool deleteTitle(App app) { return true; } -bool launchTitle(App app) { - uiDisplayMessage(TOP_SCREEN, "Launching title..."); - AppResult ret = appLaunch(app); +bool launchTitle(app::App app) { + uiDisplayMessage(gpu::SCREEN_TOP, "Launching title..."); + app::AppResult ret = app::launch(app); - if(ret != APP_SUCCESS) { + if(ret != app::APP_SUCCESS) { std::stringstream resultMsg; resultMsg << "Launch failed!" << "\n"; - resultMsg << appGetResultString(ret); - uiPrompt(TOP_SCREEN, resultMsg.str(), false); + resultMsg << app::resultString(ret); + uiPrompt(gpu::SCREEN_TOP, resultMsg.str(), false); return false; } @@ -272,28 +296,28 @@ bool launchTitle(App app) { } bool onLoop() { - bool launcher = platformHasLauncher(); - if(launcher && inputIsPressed(BUTTON_START)) { + bool launcher = core::launcher(); + if(launcher && hid::pressed(hid::BUTTON_START)) { exit = true; return true; } bool breakLoop = false; - if(inputIsPressed(BUTTON_L)) { - if(destination == SD) { - destination = NAND; + if(hid::pressed(hid::BUTTON_L)) { + if(destination == fs::SD) { + destination = fs::NAND; } else { - destination = SD; + destination = fs::SD; } - freeSpace = fsGetFreeSpace(destination); + freeSpace = fs::freeSpace(destination); if(mode == DELETE_TITLE || mode == LAUNCH_TITLE) { breakLoop = true; } } - if(inputIsPressed(BUTTON_R)) { + if(hid::pressed(hid::BUTTON_R)) { if(mode == INSTALL_CIA) { mode = DELETE_CIA; } else if(mode == DELETE_CIA) { @@ -307,29 +331,29 @@ bool onLoop() { } } - if(mode == INSTALL_CIA && inputIsPressed(BUTTON_Y)) { + if(mode == INSTALL_CIA && hid::pressed(hid::BUTTON_Y)) { networkInstall(); } - if(inputIsPressed(BUTTON_SELECT)) { + if(hid::pressed(hid::BUTTON_SELECT)) { installROP(); } std::stringstream stream; stream << "Battery: "; - if(platformIsBatteryCharging()) { + if(battery::charging()) { stream << "Charging"; } else { - for(u32 i = 0; i < platformGetBatteryLevel(); i++) { + for(u32 i = 0; i < battery::level(); i++) { stream << "|"; } } stream << ", WiFi: "; - if(!platformIsWifiConnected()) { + if(!wifi::connected()) { stream << "Disconnected"; } else { - for(u32 i = 0; i < platformGetWifiLevel(); i++) { + for(u32 i = 0; i < wifi::strength(); i++) { stream << "|"; } } @@ -337,7 +361,7 @@ bool onLoop() { stream << "\n"; stream << "Free Space: " << freeSpace << " bytes (" << std::fixed << std::setprecision(2) << freeSpace / 1024.0f / 1024.0f << "MB)" << "\n"; - stream << "Destination: " << (destination == NAND ? "NAND" : "SD") << ", Mode: " << (mode == INSTALL_CIA ? "Install CIA" : mode == DELETE_CIA ? "Delete CIA" : mode == DELETE_TITLE ? "Delete Title" : "Launch Title") << "\n"; + stream << "Destination: " << (destination == fs::NAND ? "NAND" : "SD") << ", Mode: " << (mode == INSTALL_CIA ? "Install CIA" : mode == DELETE_CIA ? "Delete CIA" : mode == DELETE_TITLE ? "Delete Title" : "Launch Title") << "\n"; stream << "L - Switch Destination, R - Switch Mode" << "\n"; if(mode == INSTALL_CIA) { stream << "X - Install all CIAs in the current directory" << "\n"; @@ -352,27 +376,34 @@ bool onLoop() { stream << "\n" << "START - Exit to launcher"; } + u32 screenWidth; + u32 screenHeight; + gpu::getViewportWidth(&screenWidth); + gpu::getViewportHeight(&screenHeight); + std::string str = stream.str(); - const std::string title = "FBI v1.4.8"; - gputDrawString(title, (gpuGetViewportWidth() - gputGetStringWidth(title, 16)) / 2, (gpuGetViewportHeight() - gputGetStringHeight(title, 16) + gputGetStringHeight(str, 8)) / 2, 16, 16); - gputDrawString(str, (gpuGetViewportWidth() - gputGetStringWidth(str, 8)) / 2, 4, 8, 8); + const std::string title = "FBI v1.4.9"; + gput::drawString(title, (screenWidth - gput::getStringWidth(title, 16)) / 2, (screenHeight - gput::getStringHeight(title, 16) + gput::getStringHeight(str, 8)) / 2, 16, 16); + gput::drawString(str, (screenWidth - gput::getStringWidth(str, 8)) / 2, 4, 8, 8); return breakLoop; } int main(int argc, char **argv) { - if(!platformInit(argc)) { + if(!core::init(argc)) { return 0; } - freeSpace = fsGetFreeSpace(destination); - while(platformIsRunning()) { + uiInit(); + + freeSpace = fs::freeSpace(destination); + while(core::running()) { if(mode == INSTALL_CIA || mode == DELETE_CIA) { - uiDisplayMessage(BOTTOM_SCREEN, "Loading file list..."); + uiDisplayMessage(gpu::SCREEN_BOTTOM, "Loading file list..."); std::string fileTarget; uiSelectFile(&fileTarget, "/", extensions, [&](const std::string currDirectory, bool inRoot, bool &updateList) { - if(inputIsPressed(BUTTON_X)) { + if(hid::pressed(hid::BUTTON_X)) { std::stringstream confirmMsg; if(mode == INSTALL_CIA) { confirmMsg << "Install "; @@ -381,13 +412,13 @@ int main(int argc, char **argv) { } confirmMsg << "all CIAs in the current directory?"; - if(uiPrompt(TOP_SCREEN, confirmMsg.str(), true)) { + if(uiPrompt(gpu::SCREEN_TOP, confirmMsg.str(), true)) { bool failed = false; - std::vector contents = fsGetDirectoryContents(currDirectory); - for(std::vector::iterator it = contents.begin(); it != contents.end(); it++) { - std::string path = (*it).path; - std::string name = (*it).name; - if(!fsIsDirectory(name) && fsHasExtensions(name, extensions)) { + std::vector contents = fs::contents(currDirectory); + for(std::vector::iterator it = contents.begin(); it != contents.end(); it++) { + std::string path = *it; + std::string name = fs::fileName(path); + if(!fs::directory(name) && fs::hasExtensions(name, extensions)) { if(mode == INSTALL_CIA) { if(!installCIA(destination, path, name)) { failed = true; @@ -413,10 +444,10 @@ int main(int argc, char **argv) { } successMsg << "succeeded!"; - uiPrompt(TOP_SCREEN, successMsg.str(), false); + uiPrompt(gpu::SCREEN_TOP, successMsg.str(), false); } - freeSpace = fsGetFreeSpace(destination); + freeSpace = fs::freeSpace(destination); } } @@ -430,12 +461,12 @@ int main(int argc, char **argv) { } confirmMsg << "the selected CIA?"; - if(uiPrompt(TOP_SCREEN, confirmMsg.str(), true)) { + if(uiPrompt(gpu::SCREEN_TOP, confirmMsg.str(), true)) { bool success = false; if(mode == INSTALL_CIA) { - success = installCIA(destination, path, fsGetFileName(path)); + success = installCIA(destination, path, fs::fileName(path)); } else { - success = deleteCIA(path, fsGetFileName(path)); + success = deleteCIA(path, fs::fileName(path)); } if(success) { @@ -447,32 +478,32 @@ int main(int argc, char **argv) { } successMsg << "succeeded!"; - uiPrompt(TOP_SCREEN, successMsg.str(), false); + uiPrompt(gpu::SCREEN_TOP, successMsg.str(), false); } - freeSpace = fsGetFreeSpace(destination); + freeSpace = fs::freeSpace(destination); } return false; }); } else if(mode == DELETE_TITLE || mode == LAUNCH_TITLE) { - uiDisplayMessage(BOTTOM_SCREEN, "Loading title list..."); + uiDisplayMessage(gpu::SCREEN_BOTTOM, "Loading title list..."); - App appTarget; + app::App appTarget; uiSelectApp(&appTarget, destination, [&](bool &updateList) { return onLoop(); - }, [&](App app, bool &updateList) { + }, [&](app::App app, bool &updateList) { if(mode == DELETE_TITLE) { - if(uiPrompt(TOP_SCREEN, "Delete the selected title?", true) && (destination != NAND || uiPrompt(TOP_SCREEN, "You are about to delete a title from the NAND.\nTHIS HAS THE POTENTIAL TO BRICK YOUR 3DS!\nAre you sure you wish to continue?", true))) { + if(uiPrompt(gpu::SCREEN_TOP, "Delete the selected title?", true) && (destination != fs::NAND || uiPrompt(gpu::SCREEN_TOP, "You are about to delete a title from the NAND.\nTHIS HAS THE POTENTIAL TO BRICK YOUR 3DS!\nAre you sure you wish to continue?", true))) { if(deleteTitle(app)) { - uiPrompt(TOP_SCREEN, "Delete succeeded!", false); + uiPrompt(gpu::SCREEN_TOP, "Delete succeeded!", false); } updateList = true; - freeSpace = fsGetFreeSpace(destination); + freeSpace = fs::freeSpace(destination); } } else if(mode == LAUNCH_TITLE) { - if(uiPrompt(TOP_SCREEN, "Launch the selected title?", true)) { + if(uiPrompt(gpu::SCREEN_TOP, "Launch the selected title?", true)) { if(launchTitle(app)) { while(true) { } @@ -489,6 +520,8 @@ int main(int argc, char **argv) { } } - platformCleanup(); + uiCleanup(); + + core::exit(); return 0; } diff --git a/source/ui.cpp b/source/ui.cpp new file mode 100644 index 0000000..f73c6a9 --- /dev/null +++ b/source/ui.cpp @@ -0,0 +1,720 @@ +#include "ui.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace ctr; + +struct uiAlphabetize { + inline bool operator()(SelectableElement a, SelectableElement b) { + return strcasecmp(a.name.c_str(), b.name.c_str()) < 0; + } +}; + +u32 selectorTexture; +u32 selectorVbo; + +void uiInit() { + gpu::createTexture(&selectorTexture); + gpu::setTextureInfo(selectorTexture, 64, 64, gpu::PIXEL_RGBA8, TEXTURE_MIN_FILTER(gpu::FILTER_NEAREST) | TEXTURE_MAG_FILTER(gpu::FILTER_NEAREST)); + + void* textureData; + gpu::getTextureData(selectorTexture, &textureData); + memset(textureData, 0xFF, 64 * 64 * 4); + + gpu::createVbo(&selectorVbo); + gpu::setVboAttributes(selectorVbo, ATTRIBUTE(0, 3, gpu::ATTR_FLOAT) | ATTRIBUTE(1, 2, gpu::ATTR_FLOAT) | ATTRIBUTE(2, 4, gpu::ATTR_FLOAT), 3); + + const float vboData[] = { + 0.0f, 0.0f, -0.1f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 320.0f, 0.0f, -0.1f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 320.0f, 12.0f, -0.1f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 320.0f, 12.0f, -0.1f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.0f, 12.0f, -0.1f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, + 0.0f, 0.0f, -0.1f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, + }; + + gpu::setVboData(selectorVbo, vboData, 6 * 9, gpu::PRIM_TRIANGLES); +} + +void uiCleanup() { + if(selectorTexture != 0) { + gpu::freeTexture(selectorTexture); + selectorTexture = 0; + } + + if(selectorVbo != 0) { + gpu::freeVbo(selectorVbo); + selectorVbo = 0; + } +} + +bool uiSelect(SelectableElement* selected, std::vector elements, std::function &currElements, SelectableElement currElement, bool &elementsDirty, bool &resetCursorIfDirty)> onLoop, std::function onSelect, bool useTopScreen, bool alphabetize, bool dpadPageScroll) { + int cursor = 0; + int scroll = 0; + + u32 selectionScroll = 0; + u64 selectionScrollEndTime = 0; + + u64 lastScrollTime = 0; + + bool elementsDirty = false; + bool resetCursorIfDirty = true; + if(alphabetize) { + std::sort(elements.begin(), elements.end(), uiAlphabetize()); + } + + hid::Button leftScrollButton = dpadPageScroll ? hid::BUTTON_LEFT : hid::BUTTON_L; + hid::Button rightScrollButton = dpadPageScroll ? hid::BUTTON_RIGHT : hid::BUTTON_R; + + bool canPageUp = false; + bool canPageDown = false; + while(core::running()) { + hid::poll(); + if(hid::pressed(hid::BUTTON_A)) { + SelectableElement select = elements.at((u32) cursor); + if(onSelect == NULL || onSelect(select)) { + *selected = select; + return true; + } + } + + if(canPageUp) { + canPageUp = !hid::released(leftScrollButton); + } else if(hid::pressed(leftScrollButton)) { + canPageUp = true; + } + + if(canPageDown) { + canPageDown = !hid::released(rightScrollButton); + } else if(hid::pressed(rightScrollButton)) { + canPageDown = true; + } + + if(hid::held(hid::BUTTON_DOWN) || hid::held(hid::BUTTON_UP) || (hid::held(leftScrollButton) && canPageUp) || (hid::held(rightScrollButton) && canPageDown)) { + if(lastScrollTime == 0 || core::time() - lastScrollTime >= 180) { + if(hid::held(hid::BUTTON_DOWN) && cursor < (int) elements.size() - 1) { + cursor++; + if(cursor >= scroll + 20) { + scroll++; + } + } + + if(canPageDown && hid::held(rightScrollButton) && cursor < (int) elements.size() - 1) { + cursor += 20; + if(cursor >= (int) elements.size()) { + cursor = elements.size() - 1; + if(cursor < 0) { + cursor = 0; + } + } + + scroll += 20; + if(scroll >= (int) elements.size() - 19) { + scroll = elements.size() - 20; + if(scroll < 0) { + scroll = 0; + } + } + } + + if(hid::held(hid::BUTTON_UP) && cursor > 0) { + cursor--; + if(cursor < scroll) { + scroll--; + } + } + + if(canPageUp && hid::held(leftScrollButton) && cursor > 0) { + cursor -= 20; + if(cursor < 0) { + cursor = 0; + } + + scroll -= 20; + if(scroll < 0) { + scroll = 0; + } + } + + selectionScroll = 0; + selectionScrollEndTime = 0; + + lastScrollTime = core::time(); + } + } else { + lastScrollTime = 0; + } + + gpu::setViewport(gpu::SCREEN_BOTTOM, 0, 0, BOTTOM_WIDTH, BOTTOM_HEIGHT); + gput::setOrtho(0, BOTTOM_WIDTH, 0, BOTTOM_HEIGHT, -1, 1); + gpu::clear(); + + u32 screenWidth; + u32 screenHeight; + gpu::getViewportWidth(&screenWidth); + gpu::getViewportHeight(&screenHeight); + + for(std::vector::iterator it = elements.begin() + scroll; it != elements.begin() + scroll + 20 && it != elements.end(); it++) { + SelectableElement element = *it; + int index = it - elements.begin(); + u8 color = 255; + int offset = 0; + float itemHeight = gput::getStringHeight(element.name, 8) + 4; + if(index == cursor) { + color = 0; + + gput::pushModelView(); + gput::translate(0, (screenHeight - 1) - ((index - scroll + 1) * itemHeight), 0); + gpu::bindTexture(gpu::TEXUNIT0, selectorTexture); + gpu::drawVbo(selectorVbo); + gput::popModelView(); + + u32 width = (u32) gput::getStringWidth(element.name, 8); + if(width > screenWidth) { + if(selectionScroll + screenWidth >= width) { + if(selectionScrollEndTime == 0) { + selectionScrollEndTime = core::time(); + } else if(core::time() - selectionScrollEndTime >= 4000) { + selectionScroll = 0; + selectionScrollEndTime = 0; + } + } else { + selectionScroll++; + } + } + + offset = -selectionScroll; + } + + gput::drawString(element.name, offset, (screenHeight - 1) - ((index - scroll + 1) * itemHeight) + 2, 8, 8, color, color, color); + } + + gpu::flushCommands(); + gpu::flushBuffer(); + + gpu::setViewport(gpu::SCREEN_TOP, 0, 0, TOP_WIDTH, TOP_HEIGHT); + gput::setOrtho(0, TOP_WIDTH, 0, TOP_HEIGHT, -1, 1); + if(useTopScreen) { + gpu::clear(); + + SelectableElement currSelected = elements.at((u32) cursor); + if(currSelected.details.size() != 0) { + std::stringstream details; + for(std::vector::iterator it = currSelected.details.begin(); it != currSelected.details.end(); it++) { + details << *it << "\n"; + } + + gput::drawString(details.str(), 0, screenHeight - 1 - gput::getStringHeight(details.str(), 8), 8, 8); + } + } + + bool result = onLoop != NULL && onLoop(elements, elements.at((u32) cursor), elementsDirty, resetCursorIfDirty); + if(elementsDirty) { + if(resetCursorIfDirty) { + cursor = 0; + scroll = 0; + } else if(cursor >= (int) elements.size()) { + cursor = elements.size() - 1; + if(cursor < 0) { + cursor = 0; + } + + scroll = elements.size() - 20; + if(scroll < 0) { + scroll = 0; + } + } + + selectionScroll = 0; + selectionScrollEndTime = 0; + if(alphabetize) { + std::sort(elements.begin(), elements.end(), uiAlphabetize()); + } + + elementsDirty = false; + resetCursorIfDirty = true; + } + + if(useTopScreen) { + gpu::flushCommands(); + gpu::flushBuffer(); + } + + gpu::swapBuffers(true); + if(result) { + break; + } + } + + return false; +} + +void uiGetDirContents(std::vector &elements, const std::string directory, std::vector extensions) { + elements.clear(); + elements.push_back({".", "."}); + elements.push_back({"..", ".."}); + + std::vector contents = fs::contents(directory); + for(std::vector::iterator it = contents.begin(); it != contents.end(); it++) { + const std::string path = *it; + const std::string name = fs::fileName(path); + if(fs::directory(path)) { + elements.push_back({path, name}); + } else if(fs::hasExtensions(path, extensions)) { + struct stat st; + stat(path.c_str(), &st); + + std::vector info; + std::stringstream stream; + stream << "File Size: " << ((u32) st.st_size) << " bytes (" << std::fixed << std::setprecision(2) << ((u32) st.st_size) / 1024.0f / 1024.0f << "MB)"; + info.push_back(stream.str()); + + std::string extension = fs::extension(path); + if(extension.compare("cia") == 0) { + app::App app; + app::ciaInfo(&app, path, fs::SD); + + std::stringstream titleId; + titleId << "0x" << std::setfill('0') << std::setw(16) << std::hex << app.titleId; + + std::stringstream uniqueId; + uniqueId << "0x" << std::setfill('0') << std::setw(8) << std::hex << app.uniqueId; + + std::stringstream version; + version << "0x" << std::setfill('0') << std::hex << app.version; + + std::stringstream size; + size << "" << app.size << " bytes (" << std::fixed << std::setprecision(2) << app.size / 1024.0f / 1024.0f << "MB)"; + + info.push_back("Installed Size: " + size.str()); + info.push_back("Title ID: " + titleId.str()); + info.push_back("Unique ID: " + uniqueId.str()); + info.push_back("Product Code: " + std::string(app.productCode)); + info.push_back("Platform: " + app::platformString(app.platform)); + info.push_back("Category: " + app::categoryString(app.category)); + info.push_back("Version: " + version.str()); + } + + elements.push_back({path, name, info}); + } + } +} + +bool uiSelectFile(std::string* selectedFile, const std::string rootDirectory, std::vector extensions, std::function onLoop, std::function onSelect, bool useTopScreen, bool dpadPageScroll) { + std::stack directoryStack; + std::string currDirectory = rootDirectory; + + std::vector elements; + uiGetDirContents(elements, currDirectory, extensions); + + bool updateContents = false; + bool resetCursor = true; + SelectableElement selected; + bool result = uiSelect(&selected, elements, [&](std::vector &currElements, SelectableElement currElement, bool &elementsDirty, bool &resetCursorIfDirty) { + if(onLoop != NULL && onLoop(currDirectory, directoryStack.empty(), updateContents)) { + return true; + } + + if(hid::pressed(hid::BUTTON_B) && !directoryStack.empty()) { + currDirectory = directoryStack.top(); + directoryStack.pop(); + updateContents = true; + } + + if(updateContents) { + uiGetDirContents(currElements, currDirectory, extensions); + elementsDirty = true; + resetCursorIfDirty = resetCursor; + updateContents = false; + resetCursor = true; + } + + return false; + }, [&](SelectableElement select) { + if(select.name.compare(".") == 0) { + return false; + } else if(select.name.compare("..") == 0) { + if(!directoryStack.empty()) { + currDirectory = directoryStack.top(); + directoryStack.pop(); + updateContents = true; + } + + return false; + } else if(fs::directory(select.id)) { + directoryStack.push(currDirectory); + currDirectory = select.id; + updateContents = true; + return false; + } + + bool updateList = false; + bool ret = onSelect(select.id, updateList); + if(updateList) { + updateContents = true; + resetCursor = false; + } + + return ret; + }, useTopScreen, true, dpadPageScroll); + + if(result) { + *selectedFile = selected.id; + } + + return result; +} + +void uiGetApps(std::vector &elements, std::vector apps) { + elements.clear(); + for(std::vector::iterator it = apps.begin(); it != apps.end(); it++) { + app::App app = *it; + + std::stringstream titleId; + titleId << "0x" << std::setfill('0') << std::setw(16) << std::hex << app.titleId; + + std::stringstream uniqueId; + uniqueId << "0x" << std::setfill('0') << std::setw(8) << std::hex << app.uniqueId; + + std::stringstream version; + version << "0x" << std::setfill('0') << std::hex << app.version; + + std::stringstream size; + size << "" << app.size << " bytes (" << std::fixed << std::setprecision(2) << app.size / 1024.0f / 1024.0f << "MB)"; + + std::vector details; + details.push_back("Title ID: " + titleId.str()); + details.push_back("Unique ID: " + uniqueId.str()); + details.push_back("Product Code: " + std::string(app.productCode)); + details.push_back("Platform: " + app::platformString(app.platform)); + details.push_back("Category: " + app::categoryString(app.category)); + details.push_back("Version: " + version.str()); + details.push_back("Size: " + size.str()); + + elements.push_back({titleId.str(), app.productCode, details}); + } + + if(elements.size() == 0) { + elements.push_back({"None", "None"}); + } +} + +bool uiFindApp(app::App* result, std::string id, std::vector apps) { + for(std::vector::iterator it = apps.begin(); it != apps.end(); it++) { + app::App app = *it; + if(app.titleId == (u64) strtoll(id.c_str(), NULL, 16)) { + *result = app; + return true; + } + } + + return false; +} + +bool uiSelectApp(app::App* selectedApp, fs::MediaType mediaType, std::function onLoop, std::function onSelect, bool useTopScreen, bool dpadPageScroll) { + std::vector elements; + + std::vector apps; + app::list(&apps, mediaType); + uiGetApps(elements, apps); + + bool updateContents = false; + SelectableElement selected; + bool result = uiSelect(&selected, elements, [&](std::vector &currElements, SelectableElement currElement, bool &elementsDirty, bool &resetCursorIfDirty) { + if(onLoop != NULL && onLoop(updateContents)) { + return true; + } + + if(updateContents) { + app::list(&apps, mediaType); + uiGetApps(currElements, apps); + elementsDirty = true; + resetCursorIfDirty = false; + updateContents = false; + } + + return false; + }, [&](SelectableElement select) { + if(select.name.compare("None") != 0) { + app::App app; + if(uiFindApp(&app, select.id, apps)) { + bool updateList = false; + bool ret = onSelect(app, updateList); + if(updateList) { + updateContents = true; + } + + return ret; + } + } + + return false; + }, useTopScreen, true, dpadPageScroll); + + if(result) { + app::App app; + if(uiFindApp(&app, selected.id, apps)) { + *selectedApp = app; + } + } + + return result; +} + +void uiDisplayMessage(gpu::Screen screen, const std::string message) { + u32 width = screen == gpu::SCREEN_TOP ? TOP_WIDTH : BOTTOM_WIDTH; + u32 height = screen == gpu::SCREEN_TOP ? TOP_HEIGHT : BOTTOM_HEIGHT; + + gpu::setViewport(screen, 0, 0, width, height); + gput::setOrtho(0, width, 0, height, -1, 1); + + gpu::clear(); + gput::drawString(message, (width - gput::getStringWidth(message, 8)) / 2, (height - gput::getStringHeight(message, 8)) / 2, 8, 8); + gpu::flushCommands(); + gpu::flushBuffer(); + gpu::swapBuffers(true); + + gpu::setViewport(gpu::SCREEN_TOP, 0, 0, TOP_WIDTH, TOP_HEIGHT); + gput::setOrtho(0, TOP_WIDTH, 0, TOP_HEIGHT, -1, 1); +} + +bool uiPrompt(gpu::Screen screen, const std::string message, bool question) { + std::stringstream stream; + stream << message << "\n"; + if(question) { + stream << "Press A to confirm, B to cancel." << "\n"; + } else { + stream << "Press Start to continue." << "\n"; + } + + bool result = false; + std::string str = stream.str(); + while(core::running()) { + hid::poll(); + if(question) { + if(hid::pressed(hid::BUTTON_A)) { + result = true; + break; + } + + if(hid::pressed(hid::BUTTON_B)) { + result = false; + break; + } + } else { + if(hid::pressed(hid::BUTTON_START)) { + result = true; + break; + } + } + + uiDisplayMessage(screen, str); + } + + hid::poll(); + return result; +} + +void uiDisplayProgress(gpu::Screen screen, const std::string operation, const std::string details, bool quickSwap, u32 progress) { + std::stringstream stream; + stream << operation << ": ["; + u32 progressBars = progress / 4; + for(u32 i = 0; i < 25; i++) { + if(i < progressBars) { + stream << '|'; + } else { + stream << ' '; + } + } + + stream << "] " << std::setfill(' ') << std::setw(3) << progress << "%" << "\n"; + stream << details << "\n"; + + std::string str = stream.str(); + + u32 width = screen == gpu::SCREEN_TOP ? TOP_WIDTH : BOTTOM_WIDTH; + u32 height = screen == gpu::SCREEN_TOP ? TOP_HEIGHT : BOTTOM_HEIGHT; + + gpu::setViewport(screen, 0, 0, width, height); + gput::setOrtho(0, width, 0, height, -1, 1); + + gpu::clear(); + gput::drawString(str, (width - gput::getStringWidth(str, 8)) / 2, (height - gput::getStringHeight(str, 8)) / 2, 8, 8); + gpu::flushCommands(); + gpu::flushBuffer(); + gpu::swapBuffers(!quickSwap); + + gpu::setViewport(gpu::SCREEN_TOP, 0, 0, TOP_WIDTH, TOP_HEIGHT); + gput::setOrtho(0, TOP_WIDTH, 0, TOP_HEIGHT, -1, 1); +} + +u64 ntohll(u64 value) { + static const int num = 42; + if(*((char*) &num) == num) { + return (((uint64_t) htonl((u32) value)) << 32) + htonl((u32) (value >> 32)); + } else { + return value; + } +} + +int socketListen(u16 port) { + int fd = socket(AF_INET, SOCK_STREAM, 0); + if(fd < 0) { + return -1; + } + + struct sockaddr_in address; + memset(&address, 0, sizeof(address)); + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_ANY); + address.sin_port = htons(port); + + if(bind(fd, (struct sockaddr*) &address, sizeof(address)) != 0) { + closesocket(fd); + return -1; + } + + int flags = fcntl(fd, F_GETFL); + if(flags == -1) { + closesocket(fd); + return -1; + } + + if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) { + closesocket(fd); + return -1; + } + + if(listen(fd, 10) != 0) { + closesocket(fd); + return -1; + } + + return fd; +} + +FILE* socketAccept(int listeningSocket) { + struct sockaddr_in addr; + socklen_t addrSize = sizeof(addr); + int afd = accept(listeningSocket, (struct sockaddr*) &addr, &addrSize); + if(afd < 0) { + return NULL; + } + + int flags = fcntl(afd, F_GETFL); + if(flags == -1) { + closesocket(afd); + return NULL; + } + + if(fcntl(afd, F_SETFL, flags | O_NONBLOCK) != 0) { + closesocket(afd); + return NULL; + } + + return fdopen(afd, "rw"); +} + +RemoteFile uiAcceptRemoteFile(gpu::Screen screen, std::function onWait) { + // meh + for(u32 i = 0; i < 2; i++) { + uiDisplayMessage(screen == gpu::SCREEN_TOP ? gpu::SCREEN_BOTTOM : gpu::SCREEN_TOP, ""); + } + + uiDisplayMessage(screen, "Initializing socket..."); + + int listen = socketListen(5000); + if(listen < 0) { + std::stringstream errStream; + errStream << "Failed to initialize socket." << "\n" << strerror(errno) << "\n"; + uiPrompt(screen, errStream.str(), false); + return {NULL, 0}; + } + + std::stringstream baseInfoStream; + baseInfoStream << "Waiting for peer to connect..." << "\n"; + baseInfoStream << "IP: " << inet_ntoa({(u32) gethostid()}) << "\n"; + baseInfoStream << "Press B to cancel." << "\n"; + std::string baseInfo = baseInfoStream.str(); + + FILE* socket; + while((socket = socketAccept(listen)) == NULL) { + if(!core::running()) { + close(listen); + return {NULL, 0}; + } + + if(errno != EAGAIN && errno != EWOULDBLOCK && errno != EINPROGRESS) { + close(listen); + + std::stringstream errStream; + errStream << "Failed to accept peer." << "\n" << strerror(errno) << "\n"; + uiPrompt(screen, errStream.str(), false); + + return {NULL, 0}; + } + + hid::poll(); + if(hid::pressed(hid::BUTTON_B)) { + close(listen); + return {NULL, 0}; + } + + std::stringstream infoStream; + infoStream << baseInfo; + onWait(infoStream); + uiDisplayMessage(screen, infoStream.str()); + } + + close(listen); + + uiDisplayMessage(screen, "Reading info...\nPress B to cancel."); + + u64 fileSize = 0; + u64 bytesRead = 0; + while(bytesRead < sizeof(fileSize)) { + if(!core::running()) { + fclose(socket); + return {NULL, 0}; + } + + size_t currBytesRead = fread(&fileSize + bytesRead, 1, (size_t) (sizeof(fileSize) - bytesRead), socket); + if(currBytesRead > 0) { + bytesRead += currBytesRead; + } + + if(ferror(socket) && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINPROGRESS) { + fclose(socket); + + std::stringstream errStream; + errStream << "Failed to read info." << "\n" << strerror(errno) << "\n"; + uiPrompt(screen, errStream.str(), false); + + return {NULL, 0}; + } + + hid::poll(); + if(hid::pressed(hid::BUTTON_B)) { + fclose(socket); + return {NULL, 0}; + } + } + + fileSize = ntohll(fileSize); + return {socket, fileSize}; +} diff --git a/source/ui.hpp b/source/ui.hpp new file mode 100644 index 0000000..2696a1a --- /dev/null +++ b/source/ui.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +typedef struct { + std::string id; + std::string name; + std::vector details; +} SelectableElement; + +typedef struct { + FILE* fd; + u64 fileSize; +} RemoteFile; + +void uiInit(); +void uiCleanup(); + +bool uiSelect(SelectableElement* selected, std::vector elements, std::function &currElements, SelectableElement currElement, bool &elementsDirty, bool &resetCursorIfDirty)> onLoop, std::function onSelect, bool useTopScreen = true, bool alphabetize = true, bool dpadPageScroll = true); +bool uiSelectFile(std::string* selectedFile, const std::string rootDirectory, std::vector extensions, std::function onLoop, std::function onSelect, bool useTopScreen = true, bool dpadPageScroll = true); +bool uiSelectApp(ctr::app::App* selectedApp, ctr::fs::MediaType mediaType, std::function onLoop, std::function onSelect, bool useTopScreen = true, bool dpadPageScroll = true); +void uiDisplayMessage(ctr::gpu::Screen screen, const std::string message); +bool uiPrompt(ctr::gpu::Screen screen, const std::string message, bool question); +void uiDisplayProgress(ctr::gpu::Screen screen, const std::string operation, const std::string details, bool quickSwap, u32 progress); +RemoteFile uiAcceptRemoteFile(ctr::gpu::Screen screen, std::function onWait = [&](std::stringstream& infoStream){});