From 7717ae268386ae0a211cecc430e93121f7dae250 Mon Sep 17 00:00:00 2001 From: Lev Roland-Kalb <114942703+Levrk@users.noreply.github.com> Date: Sat, 21 Feb 2026 04:27:25 -0500 Subject: [PATCH] feat: Added BmpViewer activity for viewing .bmp images in file browser (#887) ## Summary * **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.) Implements new feature for viewing .bmp files directly from the "Browse Files" menu. * **What changes are included?** You can now view .bmp files when browsing. You can click the select button to open the file, and then click back to close it and continue browsing in the same location. Once open a file will display on the screen with no additional options to interact outside of exiting with the back button. The attached video shows this feature in action: https://github.com/user-attachments/assets/9659b6da-abf7-4458-b158-e11c248c8bef ## Additional Context * Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on). The changes implemented in #884 are also present here as this feature is actually what led to me noticing this issue. I figured I would add that PR as a separate request in case that one could be more easily merged given this feature is significantly more complicated and will likely be subject to more intense review. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? **YES** --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/activities/home/MyLibraryActivity.cpp | 2 +- src/activities/reader/ReaderActivity.cpp | 13 ++- src/activities/reader/ReaderActivity.h | 2 + src/activities/util/BmpViewerActivity.cpp | 101 ++++++++++++++++++++++ src/activities/util/BmpViewerActivity.h | 21 +++++ src/components/themes/lyra/LyraTheme.cpp | 5 +- 6 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 src/activities/util/BmpViewerActivity.cpp create mode 100644 src/activities/util/BmpViewerActivity.h diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 8b44d7d1..1793e1b8 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -92,7 +92,7 @@ void MyLibraryActivity::loadFiles() { auto filename = std::string(name); if (StringUtils::checkFileExtension(filename, ".epub") || StringUtils::checkFileExtension(filename, ".xtch") || StringUtils::checkFileExtension(filename, ".xtc") || StringUtils::checkFileExtension(filename, ".txt") || - StringUtils::checkFileExtension(filename, ".md")) { + StringUtils::checkFileExtension(filename, ".md") || StringUtils::checkFileExtension(filename, ".bmp")) { files.emplace_back(filename); } } diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index f2f9199a..9c191f15 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -9,6 +9,7 @@ #include "TxtReaderActivity.h" #include "Xtc.h" #include "XtcReaderActivity.h" +#include "activities/util/BmpViewerActivity.h" #include "activities/util/FullScreenMessageActivity.h" #include "util/StringUtils.h" @@ -29,6 +30,8 @@ bool ReaderActivity::isTxtFile(const std::string& path) { StringUtils::checkFileExtension(path, ".md"); // Treat .md as txt files (until we have a markdown reader) } +bool ReaderActivity::isBmpFile(const std::string& path) { return StringUtils::checkFileExtension(path, ".bmp"); } + std::unique_ptr ReaderActivity::loadEpub(const std::string& path) { if (!Storage.exists(path.c_str())) { LOG_ERR("READER", "File does not exist: %s", path.c_str()); @@ -88,6 +91,11 @@ void ReaderActivity::onGoToEpubReader(std::unique_ptr epub) { renderer, mappedInput, std::move(epub), [this, epubPath] { goToLibrary(epubPath); }, [this] { onGoBack(); })); } +void ReaderActivity::onGoToBmpViewer(const std::string& path) { + exitActivity(); + enterNewActivity(new BmpViewerActivity(renderer, mappedInput, path, [this, path] { goToLibrary(path); })); +} + void ReaderActivity::onGoToXtcReader(std::unique_ptr xtc) { const auto xtcPath = xtc->getPath(); currentBookPath = xtcPath; @@ -113,8 +121,9 @@ void ReaderActivity::onEnter() { } currentBookPath = initialBookPath; - - if (isXtcFile(initialBookPath)) { + if (isBmpFile(initialBookPath)) { + onGoToBmpViewer(initialBookPath); + } else if (isXtcFile(initialBookPath)) { auto xtc = loadXtc(initialBookPath); if (!xtc) { onGoBack(); diff --git a/src/activities/reader/ReaderActivity.h b/src/activities/reader/ReaderActivity.h index 5a2c1012..52a3e1d1 100644 --- a/src/activities/reader/ReaderActivity.h +++ b/src/activities/reader/ReaderActivity.h @@ -18,12 +18,14 @@ class ReaderActivity final : public ActivityWithSubactivity { static std::unique_ptr loadTxt(const std::string& path); static bool isXtcFile(const std::string& path); static bool isTxtFile(const std::string& path); + static bool isBmpFile(const std::string& path); static std::string extractFolderPath(const std::string& filePath); void goToLibrary(const std::string& fromBookPath = ""); void onGoToEpubReader(std::unique_ptr epub); void onGoToXtcReader(std::unique_ptr xtc); void onGoToTxtReader(std::unique_ptr txt); + void onGoToBmpViewer(const std::string& path); public: explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath, diff --git a/src/activities/util/BmpViewerActivity.cpp b/src/activities/util/BmpViewerActivity.cpp new file mode 100644 index 00000000..46dec3c7 --- /dev/null +++ b/src/activities/util/BmpViewerActivity.cpp @@ -0,0 +1,101 @@ +#include "BmpViewerActivity.h" + +#include +#include +#include +#include + +#include "components/UITheme.h" +#include "fontIds.h" + +BmpViewerActivity::BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string path, + std::function onGoBack) + : Activity("BmpViewer", renderer, mappedInput), filePath(std::move(path)), onGoBack(std::move(onGoBack)) {} + +void BmpViewerActivity::onEnter() { + Activity::onEnter(); + // Removed the redundant initial renderer.clearScreen() + + FsFile file; + + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + Rect popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); + GUI.fillPopupProgress(renderer, popupRect, 20); // Initial 20% progress + // 1. Open the file + if (Storage.openFileForRead("BMP", filePath, file)) { + Bitmap bitmap(file, true); + + // 2. Parse headers to get dimensions + if (bitmap.parseHeaders() == BmpReaderError::Ok) { + int x, y; + + if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) { + float ratio = static_cast(bitmap.getWidth()) / static_cast(bitmap.getHeight()); + const float screenRatio = static_cast(pageWidth) / static_cast(pageHeight); + + if (ratio > screenRatio) { + // Wider than screen + x = 0; + y = std::round((static_cast(pageHeight) - static_cast(pageWidth) / ratio) / 2); + } else { + // Taller than screen + x = std::round((static_cast(pageWidth) - static_cast(pageHeight) * ratio) / 2); + y = 0; + } + } else { + // Center small images + x = (pageWidth - bitmap.getWidth()) / 2; + y = (pageHeight - bitmap.getHeight()) / 2; + } + + // 4. Prepare Rendering + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + GUI.fillPopupProgress(renderer, popupRect, 50); + + renderer.clearScreen(); + // Assuming drawBitmap defaults to 0,0 crop if omitted, or pass explicitly: drawBitmap(bitmap, x, y, pageWidth, + // pageHeight, 0, 0) + renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, 0, 0); + + // Draw UI hints on the base layer + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + // Single pass for non-grayscale images + + renderer.displayBuffer(HalDisplay::FULL_REFRESH); + + } else { + // Handle file parsing error + renderer.clearScreen(); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Invalid BMP File"); + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); + } + + file.close(); + } else { + // Handle file open error + renderer.clearScreen(); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Could not open file"); + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + renderer.displayBuffer(HalDisplay::FULL_REFRESH); + } +} + +void BmpViewerActivity::onExit() { + Activity::onExit(); + renderer.clearScreen(); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); +} + +void BmpViewerActivity::loop() { + // Keep CPU awake/polling so 1st click works + Activity::loop(); + + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + if (onGoBack) onGoBack(); + return; + } +} \ No newline at end of file diff --git a/src/activities/util/BmpViewerActivity.h b/src/activities/util/BmpViewerActivity.h new file mode 100644 index 00000000..c3fba379 --- /dev/null +++ b/src/activities/util/BmpViewerActivity.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +#include "../Activity.h" +#include "MappedInputManager.h" + +class BmpViewerActivity final : public Activity { + public: + BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string filePath, + std::function onGoBack); + + void onEnter() override; + void onExit() override; + void loop() override; + + private: + std::string filePath; + std::function onGoBack; +}; \ No newline at end of file diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index a291b7c4..3f0898c4 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -355,7 +355,7 @@ void LyraTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const c const int x = buttonPositions[i]; if (labels[i] != nullptr && labels[i][0] != '\0') { // Draw the filled background and border for a FULL-sized button - renderer.fillRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, false); + renderer.fillRoundedRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, cornerRadius, Color::White); renderer.drawRoundedRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, 1, cornerRadius, true, true, false, false, true); const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, labels[i]); @@ -363,7 +363,8 @@ void LyraTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const c renderer.drawText(SMALL_FONT_ID, textX, pageHeight - buttonY + textYOffset, labels[i]); } else { // Draw the filled background and border for a SMALL-sized button - renderer.fillRect(x, pageHeight - smallButtonHeight, buttonWidth, smallButtonHeight, false); + renderer.fillRoundedRect(x, pageHeight - smallButtonHeight, buttonWidth, smallButtonHeight, cornerRadius, + Color::White); renderer.drawRoundedRect(x, pageHeight - smallButtonHeight, buttonWidth, smallButtonHeight, 1, cornerRadius, true, true, false, false, true); }