feat: Current page as QR (#1099)
## Summary * **What is the goal of this PR?** Implements QR text of the current page * **What changes are included?** ## Additional Context I saw this feature request at #982 It made sense to me so I implemented. But if the team thinks it is not necessary please let me know and we can close the PR. | Page | Menu | QR | |------|-------|----| |  |  |  | --- ### AI Usage Did you use AI tools to help write this code? _** YES --------- Co-authored-by: Eliz Kilic <elizk@google.com>
This commit is contained in:
@@ -32,6 +32,7 @@ class PageLine final : public PageElement {
|
||||
public:
|
||||
PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos)
|
||||
: PageElement(xPos, yPos), block(std::move(block)) {}
|
||||
const std::shared_ptr<TextBlock>& getBlock() const { return block; }
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||
bool serialize(FsFile& file) override;
|
||||
PageElementTag getTag() const override { return TAG_PageLine; }
|
||||
|
||||
@@ -27,6 +27,7 @@ class TextBlock final : public Block {
|
||||
~TextBlock() override = default;
|
||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
||||
const std::vector<std::string>& getWords() const { return words; }
|
||||
bool isEmpty() override { return words.empty(); }
|
||||
// given a renderer works out where to break the words into lines
|
||||
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
||||
|
||||
@@ -285,6 +285,7 @@ STR_GO_TO_PERCENT: "Go to %"
|
||||
STR_GO_HOME_BUTTON: "Go Home"
|
||||
STR_SYNC_PROGRESS: "Sync Progress"
|
||||
STR_DELETE_CACHE: "Delete Book Cache"
|
||||
STR_DISPLAY_QR: "Show page as QR"
|
||||
STR_CHAPTER_PREFIX: "Chapter: "
|
||||
STR_PAGES_SEPARATOR: " pages | "
|
||||
STR_BOOK_PREFIX: "Book: "
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include <I18n.h>
|
||||
#include <WiFi.h>
|
||||
#include <esp_task_wdt.h>
|
||||
#include <qrcode.h>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
@@ -16,6 +15,7 @@
|
||||
#include "activities/network/CalibreConnectActivity.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "util/QrUtils.h"
|
||||
|
||||
namespace {
|
||||
// AP Mode configuration
|
||||
@@ -24,8 +24,8 @@ constexpr const char* AP_PASSWORD = nullptr; // Open network for ease of use
|
||||
constexpr const char* AP_HOSTNAME = "crosspoint";
|
||||
constexpr uint8_t AP_CHANNEL = 1;
|
||||
constexpr uint8_t AP_MAX_CONNECTIONS = 4;
|
||||
constexpr int QR_CODE_WIDTH = 6 * 33;
|
||||
constexpr int QR_CODE_HEIGHT = 200;
|
||||
constexpr int QR_CODE_WIDTH = 198;
|
||||
constexpr int QR_CODE_HEIGHT = 198;
|
||||
|
||||
// DNS server for captive portal (redirects all DNS queries to our IP)
|
||||
DNSServer* dnsServer = nullptr;
|
||||
@@ -363,28 +363,6 @@ void CrossPointWebServerActivity::render(Activity::RenderLock&&) {
|
||||
}
|
||||
}
|
||||
|
||||
void drawQRCode(const GfxRenderer& renderer, const int x, const int y, const std::string& data) {
|
||||
// Implementation of QR code calculation
|
||||
// The structure to manage the QR code
|
||||
QRCode qrcode;
|
||||
uint8_t qrcodeBytes[qrcode_getBufferSize(4)];
|
||||
LOG_DBG("WEBACT", "QR Code (%lu): %s", data.length(), data.c_str());
|
||||
|
||||
qrcode_initText(&qrcode, qrcodeBytes, 4, ECC_LOW, data.c_str());
|
||||
const uint8_t px = 6; // pixels per module
|
||||
for (uint8_t cy = 0; cy < qrcode.size; cy++) {
|
||||
for (uint8_t cx = 0; cx < qrcode.size; cx++) {
|
||||
if (qrcode_getModule(&qrcode, cx, cy)) {
|
||||
// Serial.print("**");
|
||||
renderer.fillRect(x + px * cx, y + px * cy, px, px, true);
|
||||
} else {
|
||||
// Serial.print(" ");
|
||||
}
|
||||
}
|
||||
// Serial.print("\n");
|
||||
}
|
||||
}
|
||||
|
||||
void CrossPointWebServerActivity::renderServerRunning() const {
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
@@ -404,7 +382,8 @@ void CrossPointWebServerActivity::renderServerRunning() const {
|
||||
|
||||
// Show QR code for Wifi
|
||||
const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;";
|
||||
drawQRCode(renderer, metrics.contentSidePadding, startY, wifiConfig);
|
||||
const Rect qrBoundsWifi(metrics.contentSidePadding, startY, QR_CODE_WIDTH, QR_CODE_HEIGHT);
|
||||
QrUtils::drawQrCode(renderer, qrBoundsWifi, wifiConfig);
|
||||
|
||||
// Show network name
|
||||
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 80,
|
||||
@@ -421,7 +400,8 @@ void CrossPointWebServerActivity::renderServerRunning() const {
|
||||
std::string ipUrl = tr(STR_OR_HTTP_PREFIX) + connectedIP + "/";
|
||||
|
||||
// Show QR code for URL
|
||||
drawQRCode(renderer, metrics.contentSidePadding, startY, hostnameUrl);
|
||||
const Rect qrBoundsUrl(metrics.contentSidePadding, startY, QR_CODE_WIDTH, QR_CODE_HEIGHT);
|
||||
QrUtils::drawQrCode(renderer, qrBoundsUrl, hostnameUrl);
|
||||
|
||||
// Show IP address as fallback
|
||||
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 80,
|
||||
@@ -440,7 +420,8 @@ void CrossPointWebServerActivity::renderServerRunning() const {
|
||||
|
||||
// Show QR code for URL
|
||||
std::string webInfo = "http://" + connectedIP + "/";
|
||||
drawQRCode(renderer, (pageWidth - QR_CODE_WIDTH) / 2, startY, webInfo);
|
||||
const Rect qrBounds((pageWidth - QR_CODE_WIDTH) / 2, startY, QR_CODE_WIDTH, QR_CODE_HEIGHT);
|
||||
QrUtils::drawQrCode(renderer, qrBounds, webInfo);
|
||||
startY += QR_CODE_HEIGHT + metrics.verticalSpacing * 2;
|
||||
|
||||
// Show web server URL prominently
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "EpubReaderActivity.h"
|
||||
|
||||
#include <Epub/Page.h>
|
||||
#include <Epub/blocks/TextBlock.h>
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
@@ -14,6 +15,7 @@
|
||||
#include "KOReaderCredentialStore.h"
|
||||
#include "KOReaderSyncActivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "QrDisplayActivity.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
@@ -403,6 +405,38 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
||||
}));
|
||||
break;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::DISPLAY_QR: {
|
||||
if (section && section->currentPage >= 0 && section->currentPage < section->pageCount) {
|
||||
auto p = section->loadPageFromSectionFile();
|
||||
if (p) {
|
||||
std::string fullText;
|
||||
for (const auto& el : p->elements) {
|
||||
if (el->getTag() == TAG_PageLine) {
|
||||
const auto& line = static_cast<const PageLine&>(*el);
|
||||
if (line.getBlock()) {
|
||||
const auto& words = line.getBlock()->getWords();
|
||||
for (const auto& w : words) {
|
||||
if (!fullText.empty()) fullText += " ";
|
||||
fullText += w;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!fullText.empty()) {
|
||||
exitActivity();
|
||||
enterNewActivity(new QrDisplayActivity(renderer, mappedInput, fullText, [this]() {
|
||||
exitActivity();
|
||||
requestUpdate();
|
||||
}));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If no text or page loading failed, just close menu
|
||||
exitActivity();
|
||||
requestUpdate();
|
||||
break;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::GO_HOME: {
|
||||
// Defer go home to avoid race condition with display task
|
||||
pendingGoHome = true;
|
||||
|
||||
@@ -12,7 +12,16 @@
|
||||
class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
||||
public:
|
||||
// Menu actions available from the reader menu.
|
||||
enum class MenuAction { SELECT_CHAPTER, GO_TO_PERCENT, ROTATE_SCREEN, SCREENSHOT, GO_HOME, SYNC, DELETE_CACHE };
|
||||
enum class MenuAction {
|
||||
SELECT_CHAPTER,
|
||||
GO_TO_PERCENT,
|
||||
ROTATE_SCREEN,
|
||||
SCREENSHOT,
|
||||
DISPLAY_QR,
|
||||
GO_HOME,
|
||||
SYNC,
|
||||
DELETE_CACHE
|
||||
};
|
||||
|
||||
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
|
||||
const int currentPage, const int totalPages, const int bookProgressPercent,
|
||||
@@ -39,11 +48,14 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
||||
};
|
||||
|
||||
// Fixed menu layout (order matters for up/down navigation).
|
||||
const std::vector<MenuItem> menuItems = {
|
||||
{MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}, {MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION},
|
||||
{MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}, {MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON},
|
||||
{MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON}, {MenuAction::SYNC, StrId::STR_SYNC_PROGRESS},
|
||||
{MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}};
|
||||
const std::vector<MenuItem> menuItems = {{MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER},
|
||||
{MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION},
|
||||
{MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT},
|
||||
{MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON},
|
||||
{MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR},
|
||||
{MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON},
|
||||
{MenuAction::SYNC, StrId::STR_SYNC_PROGRESS},
|
||||
{MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}};
|
||||
int selectedIndex = 0;
|
||||
|
||||
ButtonNavigator buttonNavigator;
|
||||
|
||||
45
src/activities/reader/QrDisplayActivity.cpp
Normal file
45
src/activities/reader/QrDisplayActivity.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#include "QrDisplayActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "util/QrUtils.h"
|
||||
|
||||
void QrDisplayActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void QrDisplayActivity::onExit() { Activity::onExit(); }
|
||||
|
||||
void QrDisplayActivity::loop() {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back) ||
|
||||
mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
onGoBack();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void QrDisplayActivity::render(Activity::RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
auto metrics = UITheme::getInstance().getMetrics();
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_DISPLAY_QR), nullptr);
|
||||
|
||||
const int availableWidth = pageWidth - 40;
|
||||
const int availableHeight = pageHeight - metrics.topPadding - metrics.headerHeight - metrics.verticalSpacing * 2 - 40;
|
||||
const int startY = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
|
||||
|
||||
const Rect qrBounds(20, startY, availableWidth, availableHeight);
|
||||
QrUtils::drawQrCode(renderer, qrBounds, textPayload);
|
||||
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
22
src/activities/reader/QrDisplayActivity.h
Normal file
22
src/activities/reader/QrDisplayActivity.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
#include <I18n.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "activities/Activity.h"
|
||||
|
||||
class QrDisplayActivity final : public Activity {
|
||||
public:
|
||||
explicit QrDisplayActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& textPayload,
|
||||
const std::function<void()>& onGoBack)
|
||||
: Activity("QrDisplay", renderer, mappedInput), textPayload(textPayload), onGoBack(onGoBack) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(Activity::RenderLock&&) override;
|
||||
|
||||
private:
|
||||
std::string textPayload;
|
||||
const std::function<void()> onGoBack;
|
||||
};
|
||||
54
src/util/QrUtils.cpp
Normal file
54
src/util/QrUtils.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#include "QrUtils.h"
|
||||
|
||||
#include <qrcode.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
|
||||
#include "Logging.h"
|
||||
|
||||
void QrUtils::drawQrCode(const GfxRenderer& renderer, const Rect& bounds, const std::string& textPayload) {
|
||||
// Dynamically calculate the QR code version based on text length
|
||||
// Version 4 holds ~114 bytes, Version 10 ~395, Version 20 ~1066, up to 40
|
||||
// qrcode.h max version is 40.
|
||||
// Formula: approx version = size / 26 + 1 (very rough estimate, better to find best fit)
|
||||
const size_t len = textPayload.length();
|
||||
int version = 4;
|
||||
if (len > 114) version = 10;
|
||||
if (len > 395) version = 20;
|
||||
if (len > 1066) version = 30;
|
||||
if (len > 2110) version = 40;
|
||||
|
||||
// Make sure we have a large enough buffer on the heap to avoid blowing the stack
|
||||
uint32_t bufferSize = qrcode_getBufferSize(version);
|
||||
auto qrcodeBytes = std::make_unique<uint8_t[]>(bufferSize);
|
||||
|
||||
QRCode qrcode;
|
||||
// Initialize the QR code. We use ECC_LOW for max capacity.
|
||||
int8_t res = qrcode_initText(&qrcode, qrcodeBytes.get(), version, ECC_LOW, textPayload.c_str());
|
||||
|
||||
if (res == 0) {
|
||||
// Determine the optimal pixel size.
|
||||
const int maxDim = std::min(bounds.width, bounds.height);
|
||||
|
||||
int px = maxDim / qrcode.size;
|
||||
if (px < 1) px = 1;
|
||||
|
||||
// Calculate centering X and Y
|
||||
const int qrDisplaySize = qrcode.size * px;
|
||||
const int xOff = bounds.x + (bounds.width - qrDisplaySize) / 2;
|
||||
const int yOff = bounds.y + (bounds.height - qrDisplaySize) / 2;
|
||||
|
||||
// Draw the QR Code
|
||||
for (uint8_t cy = 0; cy < qrcode.size; cy++) {
|
||||
for (uint8_t cx = 0; cx < qrcode.size; cx++) {
|
||||
if (qrcode_getModule(&qrcode, cx, cy)) {
|
||||
renderer.fillRect(xOff + px * cx, yOff + px * cy, px, px, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If it fails (e.g. text too large), log an error
|
||||
LOG_ERR("QR", "Text too large for QR Code version %d", version);
|
||||
}
|
||||
}
|
||||
14
src/util/QrUtils.h
Normal file
14
src/util/QrUtils.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "components/themes/BaseTheme.h"
|
||||
|
||||
namespace QrUtils {
|
||||
|
||||
// Renders a QR code with the given text payload within the specified bounding box.
|
||||
void drawQrCode(const GfxRenderer& renderer, const Rect& bounds, const std::string& textPayload);
|
||||
|
||||
} // namespace QrUtils
|
||||
Reference in New Issue
Block a user