#include "BmpViewerActivity.h" #include #include #include #include #include #include #include #include "../reader/ReaderUtils.h" #include "CrossPointSettings.h" #include "components/UITheme.h" #include "fontIds.h" #include "util/ScreenshotUtil.h" namespace { constexpr const char* SLEEP_BMP_PATH = "/sleep.bmp"; bool isBmpFile(const std::string& path) { return FsHelpers::hasBmpExtension(path); } bool isSupportedImageFile(const std::string& path) { return FsHelpers::hasBmpExtension(path) || FsHelpers::hasJpgExtension(path) || FsHelpers::hasPngExtension(path); } void computeCenteredImagePlacement(const int imageWidth, const int imageHeight, const int pageWidth, const int pageHeight, int& x, int& y, int& renderWidth, int& renderHeight) { renderWidth = imageWidth; renderHeight = imageHeight; if (imageWidth > pageWidth || imageHeight > pageHeight) { const float ratio = static_cast(imageWidth) / static_cast(imageHeight); const float screenRatio = static_cast(pageWidth) / static_cast(pageHeight); if (ratio > screenRatio) { renderWidth = pageWidth; renderHeight = std::round(static_cast(pageWidth) / ratio); x = 0; y = std::round((static_cast(pageHeight) - renderHeight) / 2.0f); } else { renderHeight = pageHeight; renderWidth = std::round(static_cast(pageHeight) * ratio); x = std::round((static_cast(pageWidth) - renderWidth) / 2.0f); y = 0; } return; } x = (pageWidth - imageWidth) / 2; y = (pageHeight - imageHeight) / 2; } } // namespace BmpViewerActivity::BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string path) : Activity("BmpViewer", renderer, mappedInput), filePath(std::move(path)) {} void BmpViewerActivity::onEnter() { Activity::onEnter(); if (!isSupportedImageFile(filePath)) { renderError("Unsupported image format"); return; } const bool rendered = isBmpFile(filePath) ? renderBmpImage() : renderDecodedImage(); if (!rendered) { renderError("Could not render image"); } } void BmpViewerActivity::onExit() { Activity::onExit(); renderer.clearScreen(); renderer.displayBuffer(HalDisplay::FULL_REFRESH); } bool BmpViewerActivity::renderBmpImage(const bool showControls) { 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); if (!Storage.openFileForRead("BMP", filePath, file)) { return false; } Bitmap bitmap(file, true); if (bitmap.parseHeaders() != BmpReaderError::Ok) { file.close(); return false; } int x, y, renderWidth, renderHeight; computeCenteredImagePlacement(bitmap.getWidth(), bitmap.getHeight(), pageWidth, pageHeight, x, y, renderWidth, renderHeight); GUI.fillPopupProgress(renderer, popupRect, 50); renderer.clearScreen(); renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, 0, 0); if (showControls) { const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", tr(STR_SET_SLEEP_SCREEN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } renderer.displayBuffer(HalDisplay::HALF_REFRESH); file.close(); return true; } bool BmpViewerActivity::renderDecodedImage(const bool showControls) { const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); Rect popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); GUI.fillPopupProgress(renderer, popupRect, 20); ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(filePath); if (!decoder) { return false; } ImageDimensions dims{}; if (!decoder->getDimensions(filePath, dims) || dims.width <= 0 || dims.height <= 0) { return false; } int x, y, renderWidth, renderHeight; computeCenteredImagePlacement(dims.width, dims.height, pageWidth, pageHeight, x, y, renderWidth, renderHeight); GUI.fillPopupProgress(renderer, popupRect, 50); renderer.clearScreen(); RenderConfig config{}; config.x = x; config.y = y; config.maxWidth = renderWidth; config.maxHeight = renderHeight; config.useExactDimensions = true; config.useGrayscale = true; config.useDithering = true; if (!decoder->decodeToFramebuffer(filePath, renderer, config)) { return false; } if (showControls) { const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", tr(STR_SET_SLEEP_SCREEN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } renderer.displayBuffer(HalDisplay::HALF_REFRESH); return true; } void BmpViewerActivity::renderError(const char* message) { const auto pageHeight = renderer.getScreenHeight(); renderer.clearScreen(); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, message); 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::setAsSleepScreen() { bool success = false; if (FsHelpers::hasBmpExtension(filePath)) { success = (filePath == SLEEP_BMP_PATH) ? true : Storage.copyFile("BMP", filePath, SLEEP_BMP_PATH); } else { const bool renderedForCapture = isBmpFile(filePath) ? renderBmpImage(false) : renderDecodedImage(false); if (renderedForCapture) { success = ScreenshotUtil::saveFramebufferAsBmp(SLEEP_BMP_PATH, renderer.getFrameBuffer(), display.getDisplayWidth(), display.getDisplayHeight()); } if (!success && Storage.exists(SLEEP_BMP_PATH)) { Storage.remove(SLEEP_BMP_PATH); } } if (!success) { LOG_ERR("BMP", "Failed to set %s as sleep screen", filePath.c_str()); GUI.drawPopup(renderer, "Failed to set sleep screen"); renderer.displayBuffer(HalDisplay::HALF_REFRESH); return; } SETTINGS.sleepScreen = CrossPointSettings::SLEEP_SCREEN_MODE::CUSTOM; SETTINGS.saveToFile(); LOG_INF("BMP", "Set %s as sleep screen", filePath.c_str()); GUI.drawPopup(renderer, tr(STR_SLEEP_SCREEN_SET)); renderer.displayBuffer(HalDisplay::HALF_REFRESH); } void BmpViewerActivity::loop() { // Keep CPU awake/polling so 1st click works Activity::loop(); // Long press BACK (1s+) goes to home screen if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) { onGoHome(); return; } // Short press BACK returns to the calling activity if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) { finish(); return; } // Next/Right button: set this image as the sleep screen if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { setAsSleepScreen(); return; } }