diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 5c2dd874..21414e57 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -94,7 +94,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..df42fc87 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()); @@ -104,6 +107,12 @@ void ReaderActivity::onGoToTxtReader(std::unique_ptr txt) { renderer, mappedInput, std::move(txt), [this, txtPath] { goToLibrary(txtPath); }, [this] { onGoBack(); })); } +void ReaderActivity::onGoToBmpViewer(const std::string& path) { + currentBookPath = path; + exitActivity(); + enterNewActivity(new BmpViewerActivity(renderer, mappedInput, path, [this, path] { goToLibrary(path); })); +} + void ReaderActivity::onEnter() { ActivityWithSubactivity::onEnter(); @@ -114,6 +123,11 @@ void ReaderActivity::onEnter() { currentBookPath = initialBookPath; + if (isBmpFile(initialBookPath)) { + onGoToBmpViewer(initialBookPath); + return; + } + if (isXtcFile(initialBookPath)) { auto xtc = loadXtc(initialBookPath); if (!xtc) { 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..21459eed --- /dev/null +++ b/src/activities/util/BmpViewerActivity.cpp @@ -0,0 +1,72 @@ +#include "BmpViewerActivity.h" + +#include +#include +#include + +#include "Bitmap.h" +#include "components/UITheme.h" +#include "fontIds.h" + +void BmpViewerActivity::onEnter() { + Activity::onEnter(); + + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + + // Show loading indicator while BMP is parsed + renderer.clearScreen(); + renderer.drawCenteredText(UI_10_FONT_ID, (pageHeight - renderer.getLineHeight(UI_10_FONT_ID)) / 2, + tr(STR_LOADING), true, EpdFontFamily::BOLD); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); + + FsFile file; + if (!Storage.openFileForRead("BMP", filePath, file)) { + LOG_ERR("BMP", "Failed to open file: %s", filePath.c_str()); + loadFailed = true; + return; + } + + Bitmap bitmap(file, true); + if (bitmap.parseHeaders() != BmpReaderError::Ok) { + LOG_ERR("BMP", "Failed to parse BMP headers: %s", filePath.c_str()); + file.close(); + loadFailed = true; + return; + } + + LOG_DBG("BMP", "Loaded %s (%d x %d)", filePath.c_str(), bitmap.getWidth(), bitmap.getHeight()); + + // Compute centered position; drawBitmap handles aspect-ratio-preserving scaling + const float ratio = static_cast(bitmap.getWidth()) / static_cast(bitmap.getHeight()); + const float screenRatio = static_cast(pageWidth) / static_cast(pageHeight); + int x, y; + if (ratio > screenRatio) { + x = 0; + y = std::round((static_cast(pageHeight) - static_cast(pageWidth) / ratio) / 2); + } else { + x = std::round((static_cast(pageWidth) - static_cast(pageHeight) * ratio) / 2); + y = 0; + } + + renderer.clearScreen(); + renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight); + file.close(); + + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + renderer.displayBuffer(HalDisplay::HALF_REFRESH); +} + +void BmpViewerActivity::loop() { + if (loadFailed) { + loadFailed = false; + onGoBack(); + return; + } + + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + onGoBack(); + } +} diff --git a/src/activities/util/BmpViewerActivity.h b/src/activities/util/BmpViewerActivity.h new file mode 100644 index 00000000..5d50ef87 --- /dev/null +++ b/src/activities/util/BmpViewerActivity.h @@ -0,0 +1,21 @@ +#pragma once +#include +#include +#include + +#include "../Activity.h" + +class BmpViewerActivity final : public Activity { + std::string filePath; + const std::function onGoBack; + bool loadFailed = false; + + public: + explicit BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string path, + std::function onGoBack) + : Activity("BmpViewer", renderer, mappedInput), + filePath(std::move(path)), + onGoBack(std::move(onGoBack)) {} + void onEnter() override; + void loop() override; +}; diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 806c5bf0..f10b15a6 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -569,7 +569,8 @@ 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, true, true, false, + false, 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]); @@ -577,7 +578,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, true, + true, false, false, Color::White); renderer.drawRoundedRect(x, pageHeight - smallButtonHeight, buttonWidth, smallButtonHeight, 1, cornerRadius, true, true, false, false, true); }