A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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>

authored by

Lev Roland-Kalb
Copilot
and committed by
GitHub
7717ae26 f02c9784

+139 -5
+1 -1
src/activities/home/MyLibraryActivity.cpp
··· 92 92 auto filename = std::string(name); 93 93 if (StringUtils::checkFileExtension(filename, ".epub") || StringUtils::checkFileExtension(filename, ".xtch") || 94 94 StringUtils::checkFileExtension(filename, ".xtc") || StringUtils::checkFileExtension(filename, ".txt") || 95 - StringUtils::checkFileExtension(filename, ".md")) { 95 + StringUtils::checkFileExtension(filename, ".md") || StringUtils::checkFileExtension(filename, ".bmp")) { 96 96 files.emplace_back(filename); 97 97 } 98 98 }
+11 -2
src/activities/reader/ReaderActivity.cpp
··· 9 9 #include "TxtReaderActivity.h" 10 10 #include "Xtc.h" 11 11 #include "XtcReaderActivity.h" 12 + #include "activities/util/BmpViewerActivity.h" 12 13 #include "activities/util/FullScreenMessageActivity.h" 13 14 #include "util/StringUtils.h" 14 15 ··· 28 29 return StringUtils::checkFileExtension(path, ".txt") || 29 30 StringUtils::checkFileExtension(path, ".md"); // Treat .md as txt files (until we have a markdown reader) 30 31 } 32 + 33 + bool ReaderActivity::isBmpFile(const std::string& path) { return StringUtils::checkFileExtension(path, ".bmp"); } 31 34 32 35 std::unique_ptr<Epub> ReaderActivity::loadEpub(const std::string& path) { 33 36 if (!Storage.exists(path.c_str())) { ··· 88 91 renderer, mappedInput, std::move(epub), [this, epubPath] { goToLibrary(epubPath); }, [this] { onGoBack(); })); 89 92 } 90 93 94 + void ReaderActivity::onGoToBmpViewer(const std::string& path) { 95 + exitActivity(); 96 + enterNewActivity(new BmpViewerActivity(renderer, mappedInput, path, [this, path] { goToLibrary(path); })); 97 + } 98 + 91 99 void ReaderActivity::onGoToXtcReader(std::unique_ptr<Xtc> xtc) { 92 100 const auto xtcPath = xtc->getPath(); 93 101 currentBookPath = xtcPath; ··· 113 121 } 114 122 115 123 currentBookPath = initialBookPath; 116 - 117 - if (isXtcFile(initialBookPath)) { 124 + if (isBmpFile(initialBookPath)) { 125 + onGoToBmpViewer(initialBookPath); 126 + } else if (isXtcFile(initialBookPath)) { 118 127 auto xtc = loadXtc(initialBookPath); 119 128 if (!xtc) { 120 129 onGoBack();
+2
src/activities/reader/ReaderActivity.h
··· 18 18 static std::unique_ptr<Txt> loadTxt(const std::string& path); 19 19 static bool isXtcFile(const std::string& path); 20 20 static bool isTxtFile(const std::string& path); 21 + static bool isBmpFile(const std::string& path); 21 22 22 23 static std::string extractFolderPath(const std::string& filePath); 23 24 void goToLibrary(const std::string& fromBookPath = ""); 24 25 void onGoToEpubReader(std::unique_ptr<Epub> epub); 25 26 void onGoToXtcReader(std::unique_ptr<Xtc> xtc); 26 27 void onGoToTxtReader(std::unique_ptr<Txt> txt); 28 + void onGoToBmpViewer(const std::string& path); 27 29 28 30 public: 29 31 explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath,
+101
src/activities/util/BmpViewerActivity.cpp
··· 1 + #include "BmpViewerActivity.h" 2 + 3 + #include <Bitmap.h> 4 + #include <GfxRenderer.h> 5 + #include <HalStorage.h> 6 + #include <I18n.h> 7 + 8 + #include "components/UITheme.h" 9 + #include "fontIds.h" 10 + 11 + BmpViewerActivity::BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string path, 12 + std::function<void()> onGoBack) 13 + : Activity("BmpViewer", renderer, mappedInput), filePath(std::move(path)), onGoBack(std::move(onGoBack)) {} 14 + 15 + void BmpViewerActivity::onEnter() { 16 + Activity::onEnter(); 17 + // Removed the redundant initial renderer.clearScreen() 18 + 19 + FsFile file; 20 + 21 + const auto pageWidth = renderer.getScreenWidth(); 22 + const auto pageHeight = renderer.getScreenHeight(); 23 + Rect popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); 24 + GUI.fillPopupProgress(renderer, popupRect, 20); // Initial 20% progress 25 + // 1. Open the file 26 + if (Storage.openFileForRead("BMP", filePath, file)) { 27 + Bitmap bitmap(file, true); 28 + 29 + // 2. Parse headers to get dimensions 30 + if (bitmap.parseHeaders() == BmpReaderError::Ok) { 31 + int x, y; 32 + 33 + if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) { 34 + float ratio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight()); 35 + const float screenRatio = static_cast<float>(pageWidth) / static_cast<float>(pageHeight); 36 + 37 + if (ratio > screenRatio) { 38 + // Wider than screen 39 + x = 0; 40 + y = std::round((static_cast<float>(pageHeight) - static_cast<float>(pageWidth) / ratio) / 2); 41 + } else { 42 + // Taller than screen 43 + x = std::round((static_cast<float>(pageWidth) - static_cast<float>(pageHeight) * ratio) / 2); 44 + y = 0; 45 + } 46 + } else { 47 + // Center small images 48 + x = (pageWidth - bitmap.getWidth()) / 2; 49 + y = (pageHeight - bitmap.getHeight()) / 2; 50 + } 51 + 52 + // 4. Prepare Rendering 53 + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); 54 + GUI.fillPopupProgress(renderer, popupRect, 50); 55 + 56 + renderer.clearScreen(); 57 + // Assuming drawBitmap defaults to 0,0 crop if omitted, or pass explicitly: drawBitmap(bitmap, x, y, pageWidth, 58 + // pageHeight, 0, 0) 59 + renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, 0, 0); 60 + 61 + // Draw UI hints on the base layer 62 + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 63 + // Single pass for non-grayscale images 64 + 65 + renderer.displayBuffer(HalDisplay::FULL_REFRESH); 66 + 67 + } else { 68 + // Handle file parsing error 69 + renderer.clearScreen(); 70 + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Invalid BMP File"); 71 + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); 72 + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 73 + renderer.displayBuffer(HalDisplay::FAST_REFRESH); 74 + } 75 + 76 + file.close(); 77 + } else { 78 + // Handle file open error 79 + renderer.clearScreen(); 80 + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Could not open file"); 81 + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); 82 + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 83 + renderer.displayBuffer(HalDisplay::FULL_REFRESH); 84 + } 85 + } 86 + 87 + void BmpViewerActivity::onExit() { 88 + Activity::onExit(); 89 + renderer.clearScreen(); 90 + renderer.displayBuffer(HalDisplay::FAST_REFRESH); 91 + } 92 + 93 + void BmpViewerActivity::loop() { 94 + // Keep CPU awake/polling so 1st click works 95 + Activity::loop(); 96 + 97 + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { 98 + if (onGoBack) onGoBack(); 99 + return; 100 + } 101 + }
+21
src/activities/util/BmpViewerActivity.h
··· 1 + #pragma once 2 + 3 + #include <functional> 4 + #include <string> 5 + 6 + #include "../Activity.h" 7 + #include "MappedInputManager.h" 8 + 9 + class BmpViewerActivity final : public Activity { 10 + public: 11 + BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string filePath, 12 + std::function<void()> onGoBack); 13 + 14 + void onEnter() override; 15 + void onExit() override; 16 + void loop() override; 17 + 18 + private: 19 + std::string filePath; 20 + std::function<void()> onGoBack; 21 + };
+3 -2
src/components/themes/lyra/LyraTheme.cpp
··· 355 355 const int x = buttonPositions[i]; 356 356 if (labels[i] != nullptr && labels[i][0] != '\0') { 357 357 // Draw the filled background and border for a FULL-sized button 358 - renderer.fillRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, false); 358 + renderer.fillRoundedRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, cornerRadius, Color::White); 359 359 renderer.drawRoundedRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, 1, cornerRadius, true, true, false, 360 360 false, true); 361 361 const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, labels[i]); ··· 363 363 renderer.drawText(SMALL_FONT_ID, textX, pageHeight - buttonY + textYOffset, labels[i]); 364 364 } else { 365 365 // Draw the filled background and border for a SMALL-sized button 366 - renderer.fillRect(x, pageHeight - smallButtonHeight, buttonWidth, smallButtonHeight, false); 366 + renderer.fillRoundedRect(x, pageHeight - smallButtonHeight, buttonWidth, smallButtonHeight, cornerRadius, 367 + Color::White); 367 368 renderer.drawRoundedRect(x, pageHeight - smallButtonHeight, buttonWidth, smallButtonHeight, 1, cornerRadius, true, 368 369 true, false, false, true); 369 370 }