Merge pull request #29 from jpirnay/feat-progress-badges
feat: Add progress badges to HomeScreen
This commit is contained in:
@@ -83,6 +83,7 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
|
|||||||
for (int i = 0; i < std::min(static_cast<int>(recentBooks.size()), Lyra3CoversMetrics::values.homeRecentBooksCount);
|
for (int i = 0; i < std::min(static_cast<int>(recentBooks.size()), Lyra3CoversMetrics::values.homeRecentBooksCount);
|
||||||
i++) {
|
i++) {
|
||||||
bool bookSelected = (selectorIndex == i);
|
bool bookSelected = (selectorIndex == i);
|
||||||
|
const int progressPercent = getRecentBookProgressPercent(recentBooks[i]);
|
||||||
|
|
||||||
int tileX = Lyra3CoversMetrics::values.contentSidePadding + tileWidth * i;
|
int tileX = Lyra3CoversMetrics::values.contentSidePadding + tileWidth * i;
|
||||||
|
|
||||||
@@ -106,6 +107,11 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
|
|||||||
cornerRadius, false, false, true, true, Color::LightGray);
|
cornerRadius, false, false, true, true, Color::LightGray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawProgressBadge(renderer,
|
||||||
|
Rect{tileX + hPaddingInSelection, tileY + hPaddingInSelection,
|
||||||
|
tileWidth - 2 * hPaddingInSelection, coverHeight},
|
||||||
|
progressPercent);
|
||||||
|
|
||||||
int currentY = tileY + coverHeight + hPaddingInSelection + 5;
|
int currentY = tileY + coverHeight + hPaddingInSelection + 5;
|
||||||
for (const auto& line : titleLines) {
|
for (const auto& line : titleLines) {
|
||||||
renderer.drawText(SMALL_FONT_ID, tileX + hPaddingInSelection, currentY, line.c_str(), true);
|
renderer.drawText(SMALL_FONT_ID, tileX + hPaddingInSelection, currentY, line.c_str(), true);
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
#include "LyraTheme.h"
|
#include "LyraTheme.h"
|
||||||
|
|
||||||
|
#include <Epub.h>
|
||||||
|
#include <FsHelpers.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HalClock.h>
|
#include <HalClock.h>
|
||||||
#include <HalGPIO.h>
|
#include <HalGPIO.h>
|
||||||
#include <HalPowerManager.h>
|
#include <HalPowerManager.h>
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
|
#include <Serialization.h>
|
||||||
|
#include <Txt.h>
|
||||||
|
#include <Xtc.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -44,8 +50,39 @@ constexpr int mainMenuIconSize = 32;
|
|||||||
constexpr int listIconSize = 24;
|
constexpr int listIconSize = 24;
|
||||||
constexpr int mainMenuColumns = 2;
|
constexpr int mainMenuColumns = 2;
|
||||||
constexpr int minAdaptiveMenuRowHeight = 40;
|
constexpr int minAdaptiveMenuRowHeight = 40;
|
||||||
|
constexpr int progressBadgeInset = 8;
|
||||||
int coverWidth = 0;
|
int coverWidth = 0;
|
||||||
|
|
||||||
|
int clampProgressPercent(const int progressPercent) {
|
||||||
|
if (progressPercent < 0) return 0;
|
||||||
|
if (progressPercent > 100) return 100;
|
||||||
|
return progressPercent;
|
||||||
|
}
|
||||||
|
|
||||||
|
int readTxtTotalPages(const std::string& cachePath) {
|
||||||
|
FsFile indexFile;
|
||||||
|
if (!Storage.openFileForRead("LYR", cachePath + "/index.bin", indexFile)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t magic = 0;
|
||||||
|
uint8_t version = 0;
|
||||||
|
serialization::readPod(indexFile, magic);
|
||||||
|
serialization::readPod(indexFile, version);
|
||||||
|
static constexpr uint32_t INDEX_CACHE_MAGIC = 0x54585449; // "TXTI"
|
||||||
|
static constexpr uint8_t INDEX_CACHE_VERSION = 2;
|
||||||
|
if (magic != INDEX_CACHE_MAGIC || version != INDEX_CACHE_VERSION) {
|
||||||
|
indexFile.close();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
indexFile.seek(32);
|
||||||
|
uint32_t totalPages = 0;
|
||||||
|
serialization::readPod(indexFile, totalPages);
|
||||||
|
indexFile.close();
|
||||||
|
return static_cast<int>(totalPages);
|
||||||
|
}
|
||||||
|
|
||||||
void drawLyraBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight,
|
void drawLyraBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight,
|
||||||
uint16_t percentage) {
|
uint16_t percentage) {
|
||||||
BaseTheme::drawBatteryOutline(renderer, x, y, battWidth, rectHeight);
|
BaseTheme::drawBatteryOutline(renderer, x, y, battWidth, rectHeight);
|
||||||
@@ -116,6 +153,111 @@ const uint8_t* iconForName(UIIcon icon, int size) {
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
int LyraTheme::getRecentBookProgressPercent(const RecentBook& book) {
|
||||||
|
if (book.path.empty()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FsHelpers::hasEpubExtension(book.path)) {
|
||||||
|
Epub epub(book.path, "/.crosspoint");
|
||||||
|
if (!epub.load(true, true)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile progressFile;
|
||||||
|
if (!Storage.openFileForRead("LYR", epub.getCachePath() + "/progress.bin", progressFile)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t data[6];
|
||||||
|
const int dataSize = progressFile.read(data, 6);
|
||||||
|
progressFile.close();
|
||||||
|
if (dataSize != 4 && dataSize != 6) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int currentSpineIndex = data[0] + (data[1] << 8);
|
||||||
|
const int currentPage = data[2] + (data[3] << 8);
|
||||||
|
const int pageCount = (dataSize == 6) ? (data[4] + (data[5] << 8)) : 0;
|
||||||
|
if (pageCount <= 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float chapterProgress = static_cast<float>(currentPage) / static_cast<float>(pageCount);
|
||||||
|
return clampProgressPercent(
|
||||||
|
static_cast<int>(std::lround(epub.calculateProgress(currentSpineIndex, chapterProgress) * 100.0f)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FsHelpers::hasXtcExtension(book.path)) {
|
||||||
|
Xtc xtc(book.path, "/.crosspoint");
|
||||||
|
if (!xtc.load()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile progressFile;
|
||||||
|
if (!Storage.openFileForRead("LYR", xtc.getCachePath() + "/progress.bin", progressFile)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t data[4];
|
||||||
|
if (progressFile.read(data, 4) != 4) {
|
||||||
|
progressFile.close();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
progressFile.close();
|
||||||
|
|
||||||
|
const uint32_t currentPage = static_cast<uint32_t>(data[0]) | (static_cast<uint32_t>(data[1]) << 8) |
|
||||||
|
(static_cast<uint32_t>(data[2]) << 16) | (static_cast<uint32_t>(data[3]) << 24);
|
||||||
|
return clampProgressPercent(static_cast<int>(xtc.calculateProgress(currentPage)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FsHelpers::hasTxtExtension(book.path) || FsHelpers::hasMarkdownExtension(book.path)) {
|
||||||
|
Txt txt(book.path, "/.crosspoint");
|
||||||
|
if (!txt.load()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile progressFile;
|
||||||
|
if (!Storage.openFileForRead("LYR", txt.getCachePath() + "/progress.bin", progressFile)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t data[4];
|
||||||
|
if (progressFile.read(data, 4) != 4) {
|
||||||
|
progressFile.close();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
progressFile.close();
|
||||||
|
|
||||||
|
const int currentPage = data[0] + (data[1] << 8);
|
||||||
|
const int totalPages = readTxtTotalPages(txt.getCachePath());
|
||||||
|
if (totalPages <= 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return clampProgressPercent(static_cast<int>(std::lround((currentPage + 1) * 100.0f / totalPages)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LyraTheme::drawProgressBadge(GfxRenderer& renderer, Rect anchorRect, int progressPercent) {
|
||||||
|
if (progressPercent < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string badgeText = std::to_string(clampProgressPercent(progressPercent)) + "%";
|
||||||
|
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, badgeText.c_str());
|
||||||
|
const int textHeight = renderer.getLineHeight(SMALL_FONT_ID);
|
||||||
|
const int badgeWidth = textWidth + 12;
|
||||||
|
const int badgeHeight = textHeight + 6;
|
||||||
|
const int badgeX = anchorRect.x + anchorRect.width - badgeWidth - progressBadgeInset;
|
||||||
|
const int badgeY = anchorRect.y + progressBadgeInset;
|
||||||
|
|
||||||
|
renderer.fillRoundedRect(badgeX, badgeY, badgeWidth, badgeHeight, 5, Color::Black);
|
||||||
|
renderer.drawText(SMALL_FONT_ID, badgeX + (badgeWidth - textWidth) / 2, badgeY + 3, badgeText.c_str(), false);
|
||||||
|
}
|
||||||
|
|
||||||
void LyraTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
|
void LyraTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
|
||||||
// Left aligned: icon on left, percentage on right (reader mode)
|
// Left aligned: icon on left, percentage on right (reader mode)
|
||||||
const uint16_t percentage = powerManager.getBatteryPercentage();
|
const uint16_t percentage = powerManager.getBatteryPercentage();
|
||||||
@@ -442,6 +584,7 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
// Only load from SD on first render, then use stored buffer
|
// Only load from SD on first render, then use stored buffer
|
||||||
if (hasContinueReading) {
|
if (hasContinueReading) {
|
||||||
RecentBook book = recentBooks[0];
|
RecentBook book = recentBooks[0];
|
||||||
|
const int progressPercent = getRecentBookProgressPercent(book);
|
||||||
if (!coverRendered) {
|
if (!coverRendered) {
|
||||||
std::string coverPath = book.coverBmpPath;
|
std::string coverPath = book.coverBmpPath;
|
||||||
bool hasCover = true;
|
bool hasCover = true;
|
||||||
@@ -496,6 +639,12 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
cornerRadius, false, false, true, true, Color::LightGray);
|
cornerRadius, false, false, true, true, Color::LightGray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawProgressBadge(
|
||||||
|
renderer,
|
||||||
|
Rect{tileX + hPaddingInSelection + coverWidth + LyraMetrics::values.verticalSpacing, tileY,
|
||||||
|
tileWidth - 2 * hPaddingInSelection - coverWidth - LyraMetrics::values.verticalSpacing, tileHeight},
|
||||||
|
progressPercent);
|
||||||
|
|
||||||
auto titleLines = renderer.wrappedText(UI_12_FONT_ID, book.title.c_str(), textWidth, 3, EpdFontFamily::BOLD);
|
auto titleLines = renderer.wrappedText(UI_12_FONT_ID, book.title.c_str(), textWidth, 3, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
auto author = renderer.truncatedText(UI_10_FONT_ID, book.author.c_str(), textWidth);
|
auto author = renderer.truncatedText(UI_10_FONT_ID, book.author.c_str(), textWidth);
|
||||||
|
|||||||
@@ -69,4 +69,8 @@ class LyraTheme : public BaseTheme {
|
|||||||
void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const override;
|
void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const override;
|
||||||
void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const override;
|
void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const override;
|
||||||
bool showsFileIcons() const override { return true; }
|
bool showsFileIcons() const override { return true; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
static int getRecentBookProgressPercent(const RecentBook& book);
|
||||||
|
static void drawProgressBadge(GfxRenderer& renderer, Rect anchorRect, int progressPercent);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user