feat: add BmpViewer activity for viewing .bmp images in file browser (port upstream PR #887)
New BmpViewerActivity opens, parses, and renders BMP files with centered aspect-ratio-preserving display and localized back navigation. Library file filter extended to include .bmp. ReaderActivity routes BMP paths to the new viewer. LyraTheme button hint backgrounds switched to rounded rect fills to prevent overflow artifacts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -94,7 +94,7 @@ void MyLibraryActivity::loadFiles() {
|
|||||||
auto filename = std::string(name);
|
auto filename = std::string(name);
|
||||||
if (StringUtils::checkFileExtension(filename, ".epub") || StringUtils::checkFileExtension(filename, ".xtch") ||
|
if (StringUtils::checkFileExtension(filename, ".epub") || StringUtils::checkFileExtension(filename, ".xtch") ||
|
||||||
StringUtils::checkFileExtension(filename, ".xtc") || StringUtils::checkFileExtension(filename, ".txt") ||
|
StringUtils::checkFileExtension(filename, ".xtc") || StringUtils::checkFileExtension(filename, ".txt") ||
|
||||||
StringUtils::checkFileExtension(filename, ".md")) {
|
StringUtils::checkFileExtension(filename, ".md") || StringUtils::checkFileExtension(filename, ".bmp")) {
|
||||||
files.emplace_back(filename);
|
files.emplace_back(filename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "TxtReaderActivity.h"
|
#include "TxtReaderActivity.h"
|
||||||
#include "Xtc.h"
|
#include "Xtc.h"
|
||||||
#include "XtcReaderActivity.h"
|
#include "XtcReaderActivity.h"
|
||||||
|
#include "activities/util/BmpViewerActivity.h"
|
||||||
#include "activities/util/FullScreenMessageActivity.h"
|
#include "activities/util/FullScreenMessageActivity.h"
|
||||||
#include "util/StringUtils.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)
|
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<Epub> ReaderActivity::loadEpub(const std::string& path) {
|
std::unique_ptr<Epub> ReaderActivity::loadEpub(const std::string& path) {
|
||||||
if (!Storage.exists(path.c_str())) {
|
if (!Storage.exists(path.c_str())) {
|
||||||
LOG_ERR("READER", "File does not exist: %s", 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> txt) {
|
|||||||
renderer, mappedInput, std::move(txt), [this, txtPath] { goToLibrary(txtPath); }, [this] { onGoBack(); }));
|
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() {
|
void ReaderActivity::onEnter() {
|
||||||
ActivityWithSubactivity::onEnter();
|
ActivityWithSubactivity::onEnter();
|
||||||
|
|
||||||
@@ -114,6 +123,11 @@ void ReaderActivity::onEnter() {
|
|||||||
|
|
||||||
currentBookPath = initialBookPath;
|
currentBookPath = initialBookPath;
|
||||||
|
|
||||||
|
if (isBmpFile(initialBookPath)) {
|
||||||
|
onGoToBmpViewer(initialBookPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isXtcFile(initialBookPath)) {
|
if (isXtcFile(initialBookPath)) {
|
||||||
auto xtc = loadXtc(initialBookPath);
|
auto xtc = loadXtc(initialBookPath);
|
||||||
if (!xtc) {
|
if (!xtc) {
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ class ReaderActivity final : public ActivityWithSubactivity {
|
|||||||
static std::unique_ptr<Txt> loadTxt(const std::string& path);
|
static std::unique_ptr<Txt> loadTxt(const std::string& path);
|
||||||
static bool isXtcFile(const std::string& path);
|
static bool isXtcFile(const std::string& path);
|
||||||
static bool isTxtFile(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);
|
static std::string extractFolderPath(const std::string& filePath);
|
||||||
void goToLibrary(const std::string& fromBookPath = "");
|
void goToLibrary(const std::string& fromBookPath = "");
|
||||||
void onGoToEpubReader(std::unique_ptr<Epub> epub);
|
void onGoToEpubReader(std::unique_ptr<Epub> epub);
|
||||||
void onGoToXtcReader(std::unique_ptr<Xtc> xtc);
|
void onGoToXtcReader(std::unique_ptr<Xtc> xtc);
|
||||||
void onGoToTxtReader(std::unique_ptr<Txt> txt);
|
void onGoToTxtReader(std::unique_ptr<Txt> txt);
|
||||||
|
void onGoToBmpViewer(const std::string& path);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath,
|
explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath,
|
||||||
|
|||||||
72
src/activities/util/BmpViewerActivity.cpp
Normal file
72
src/activities/util/BmpViewerActivity.cpp
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#include "BmpViewerActivity.h"
|
||||||
|
|
||||||
|
#include <HalDisplay.h>
|
||||||
|
#include <HalStorage.h>
|
||||||
|
#include <I18n.h>
|
||||||
|
|
||||||
|
#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<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
|
||||||
|
const float screenRatio = static_cast<float>(pageWidth) / static_cast<float>(pageHeight);
|
||||||
|
int x, y;
|
||||||
|
if (ratio > screenRatio) {
|
||||||
|
x = 0;
|
||||||
|
y = std::round((static_cast<float>(pageHeight) - static_cast<float>(pageWidth) / ratio) / 2);
|
||||||
|
} else {
|
||||||
|
x = std::round((static_cast<float>(pageWidth) - static_cast<float>(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/activities/util/BmpViewerActivity.h
Normal file
21
src/activities/util/BmpViewerActivity.h
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "../Activity.h"
|
||||||
|
|
||||||
|
class BmpViewerActivity final : public Activity {
|
||||||
|
std::string filePath;
|
||||||
|
const std::function<void()> onGoBack;
|
||||||
|
bool loadFailed = false;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string path,
|
||||||
|
std::function<void()> onGoBack)
|
||||||
|
: Activity("BmpViewer", renderer, mappedInput),
|
||||||
|
filePath(std::move(path)),
|
||||||
|
onGoBack(std::move(onGoBack)) {}
|
||||||
|
void onEnter() override;
|
||||||
|
void loop() override;
|
||||||
|
};
|
||||||
@@ -569,7 +569,8 @@ void LyraTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const c
|
|||||||
const int x = buttonPositions[i];
|
const int x = buttonPositions[i];
|
||||||
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
||||||
// Draw the filled background and border for a FULL-sized button
|
// 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,
|
renderer.drawRoundedRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, 1, cornerRadius, true, true, false,
|
||||||
false, true);
|
false, true);
|
||||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, labels[i]);
|
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]);
|
renderer.drawText(SMALL_FONT_ID, textX, pageHeight - buttonY + textYOffset, labels[i]);
|
||||||
} else {
|
} else {
|
||||||
// Draw the filled background and border for a SMALL-sized button
|
// 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,
|
renderer.drawRoundedRect(x, pageHeight - smallButtonHeight, buttonWidth, smallButtonHeight, 1, cornerRadius, true,
|
||||||
true, false, false, true);
|
true, false, false, true);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user