#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}; }