feat: Add status bar option "Full w/ Progress Bar" (#438)

## Summary

* **What is the goal of this PR?** This PR introduces a new "Status Bar"
mode that displays a visual progress bar at the bottom of the screen,
providing readers with a graphical indication of their position within
the book.
* **What changes are included?** 

* **Settings**: Updated SettingsActivity to expand the "Status Bar"
configuration with a new option: Full w/ Progress Bar.
* **EPUB Reader**: Modified EpubReaderActivity to calculate the global
book progress and render a progress bar at the bottom of the viewable
area when the new setting is active.
* **TXT Reader**: Modified TxtReaderActivity to implement similar
progress bar rendering logic based on the current page and total page
count.

## Additional Context

* The progress bar is rendered with a height of 4 pixels at the very
bottom of the screen (adjusted for margins).
* The feature reuses the existing renderStatusBar logic but
conditionally draws the bar instead of (or in addition to) other
elements depending on the specific implementation details in each
reader.
  * Renamed existing 'Full' mode to 'Full w/ Percentage'
  * Added new 'Full w/ Progress Bar' option

<img
src="https://github.com/user-attachments/assets/08c0dd49-c64c-4d4d-9fbb-f576c02d05d9"
width="500">


---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
This commit is contained in:
Alex Faria 2026-01-27 12:25:44 +00:00 committed by GitHub
parent dd1741bf0b
commit e9c2fe1c87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 105 additions and 26 deletions

View File

@ -19,7 +19,14 @@ class CrossPointSettings {
enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT }; enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT };
// Status bar display type enum // Status bar display type enum
enum STATUS_BAR_MODE { NONE = 0, NO_PROGRESS = 1, FULL = 2, STATUS_BAR_MODE_COUNT }; enum STATUS_BAR_MODE {
NONE = 0,
NO_PROGRESS = 1,
FULL = 2,
FULL_WITH_PROGRESS_BAR = 3,
ONLY_PROGRESS_BAR = 4,
STATUS_BAR_MODE_COUNT
};
enum ORIENTATION { enum ORIENTATION {
PORTRAIT = 0, // 480x800 logical coordinates (current default) PORTRAIT = 0, // 480x800 logical coordinates (current default)

View File

@ -42,6 +42,17 @@ void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left,
renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4); renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4);
} }
void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const size_t bookProgress) {
int vieweableMarginTop, vieweableMarginRight, vieweableMarginBottom, vieweableMarginLeft;
renderer.getOrientedViewableTRBL(&vieweableMarginTop, &vieweableMarginRight, &vieweableMarginBottom,
&vieweableMarginLeft);
const int progressBarMaxWidth = renderer.getScreenWidth() - vieweableMarginLeft - vieweableMarginRight;
const int progressBarY = renderer.getScreenHeight() - vieweableMarginBottom - BOOK_PROGRESS_BAR_HEIGHT;
const int barWidth = progressBarMaxWidth * bookProgress / 100;
renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BOOK_PROGRESS_BAR_HEIGHT, true);
}
int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector<TabInfo>& tabs) { int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector<TabInfo>& tabs) {
constexpr int tabPadding = 20; // Horizontal padding between tabs constexpr int tabPadding = 20; // Horizontal padding between tabs
constexpr int leftMargin = 20; // Left margin for first tab constexpr int leftMargin = 20; // Left margin for first tab

View File

@ -13,7 +13,10 @@ struct TabInfo {
class ScreenComponents { class ScreenComponents {
public: public:
static const int BOOK_PROGRESS_BAR_HEIGHT = 4;
static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true); static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true);
static void drawBookProgressBar(const GfxRenderer& renderer, size_t bookProgress);
// Draw a horizontal tab bar with underline indicator for selected tab // Draw a horizontal tab bar with underline indicator for selected tab
// Returns the height of the tab bar (for positioning content below) // Returns the height of the tab bar (for positioning content below)

View File

@ -18,6 +18,8 @@ namespace {
constexpr unsigned long skipChapterMs = 700; constexpr unsigned long skipChapterMs = 700;
constexpr unsigned long goHomeMs = 1000; constexpr unsigned long goHomeMs = 1000;
constexpr int statusBarMargin = 19; constexpr int statusBarMargin = 19;
constexpr int progressBarMarginTop = 1;
} // namespace } // namespace
void EpubReaderActivity::taskTrampoline(void* param) { void EpubReaderActivity::taskTrampoline(void* param) {
@ -275,7 +277,16 @@ void EpubReaderActivity::renderScreen() {
orientedMarginTop += SETTINGS.screenMargin; orientedMarginTop += SETTINGS.screenMargin;
orientedMarginLeft += SETTINGS.screenMargin; orientedMarginLeft += SETTINGS.screenMargin;
orientedMarginRight += SETTINGS.screenMargin; orientedMarginRight += SETTINGS.screenMargin;
orientedMarginBottom += statusBarMargin; orientedMarginBottom += SETTINGS.screenMargin;
// Add status bar margin
if (SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) {
// Add additional margin for status bar if progress bar is shown
const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR;
orientedMarginBottom += statusBarMargin - SETTINGS.screenMargin +
(showProgressBar ? (ScreenComponents::BOOK_PROGRESS_BAR_HEIGHT + progressBarMarginTop) : 0);
}
if (!section) { if (!section) {
const auto filepath = epub->getSpineItem(currentSpineIndex).href; const auto filepath = epub->getSpineItem(currentSpineIndex).href;
@ -446,11 +457,17 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const int orientedMarginBottom, void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const int orientedMarginBottom,
const int orientedMarginLeft) const { const int orientedMarginLeft) const {
// determine visible status bar elements // determine visible status bar elements
const bool showProgress = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; const bool showProgressPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL;
const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR;
const bool showProgressText = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR;
const bool showBattery = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS || const bool showBattery = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR;
const bool showChapterTitle = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS || const bool showChapterTitle = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR;
const bool showBatteryPercentage = const bool showBatteryPercentage =
SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER; SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER;
@ -459,19 +476,30 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
const auto textY = screenHeight - orientedMarginBottom - 4; const auto textY = screenHeight - orientedMarginBottom - 4;
int progressTextWidth = 0; int progressTextWidth = 0;
if (showProgress) { // Calculate progress in book
// Calculate progress in book const float sectionChapterProg = static_cast<float>(section->currentPage) / section->pageCount;
const float sectionChapterProg = static_cast<float>(section->currentPage) / section->pageCount; const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100;
const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100;
if (showProgressText || showProgressPercentage) {
// Right aligned text for progress counter // Right aligned text for progress counter
char progressStr[32]; char progressStr[32];
snprintf(progressStr, sizeof(progressStr), "%d/%d %.0f%%", section->currentPage + 1, section->pageCount,
bookProgress); // Hide percentage when progress bar is shown to reduce clutter
const std::string progress = progressStr; if (showProgressPercentage) {
progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progress.c_str()); snprintf(progressStr, sizeof(progressStr), "%d/%d %.0f%%", section->currentPage + 1, section->pageCount,
bookProgress);
} else {
snprintf(progressStr, sizeof(progressStr), "%d/%d", section->currentPage + 1, section->pageCount);
}
progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr);
renderer.drawText(SMALL_FONT_ID, renderer.getScreenWidth() - orientedMarginRight - progressTextWidth, textY, renderer.drawText(SMALL_FONT_ID, renderer.getScreenWidth() - orientedMarginRight - progressTextWidth, textY,
progress.c_str()); progressStr);
}
if (showProgressBar) {
// Draw progress bar at the very bottom of the screen, from edge to edge of viewable area
ScreenComponents::drawBookProgressBar(renderer, static_cast<size_t>(bookProgress));
} }
if (showBattery) { if (showBattery) {

View File

@ -15,6 +15,7 @@
namespace { namespace {
constexpr unsigned long goHomeMs = 1000; constexpr unsigned long goHomeMs = 1000;
constexpr int statusBarMargin = 25; constexpr int statusBarMargin = 25;
constexpr int progressBarMarginTop = 1;
constexpr size_t CHUNK_SIZE = 8 * 1024; // 8KB chunk for reading constexpr size_t CHUNK_SIZE = 8 * 1024; // 8KB chunk for reading
// Cache file magic and version // Cache file magic and version
@ -158,7 +159,16 @@ void TxtReaderActivity::initializeReader() {
orientedMarginTop += cachedScreenMargin; orientedMarginTop += cachedScreenMargin;
orientedMarginLeft += cachedScreenMargin; orientedMarginLeft += cachedScreenMargin;
orientedMarginRight += cachedScreenMargin; orientedMarginRight += cachedScreenMargin;
orientedMarginBottom += statusBarMargin; orientedMarginBottom += cachedScreenMargin;
// Add status bar margin
if (SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) {
// Add additional margin for status bar if progress bar is shown
const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR;
orientedMarginBottom += statusBarMargin - cachedScreenMargin +
(showProgressBar ? (ScreenComponents::BOOK_PROGRESS_BAR_HEIGHT + progressBarMarginTop) : 0);
}
viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight; viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
const int viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom; const int viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
@ -499,27 +509,46 @@ void TxtReaderActivity::renderPage() {
void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int orientedMarginBottom, void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int orientedMarginBottom,
const int orientedMarginLeft) const { const int orientedMarginLeft) const {
const bool showProgress = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; const bool showProgressPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL;
const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR;
const bool showProgressText = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR;
const bool showBattery = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS || const bool showBattery = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR;
const bool showTitle = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS || const bool showTitle = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL ||
SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR;
const bool showBatteryPercentage =
SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER;
const auto screenHeight = renderer.getScreenHeight(); const auto screenHeight = renderer.getScreenHeight();
const auto textY = screenHeight - orientedMarginBottom - 4; const auto textY = screenHeight - orientedMarginBottom - 4;
int progressTextWidth = 0; int progressTextWidth = 0;
if (showProgress) { const float progress = totalPages > 0 ? (currentPage + 1) * 100.0f / totalPages : 0;
const int progress = totalPages > 0 ? (currentPage + 1) * 100 / totalPages : 0;
const std::string progressStr = if (showProgressText || showProgressPercentage) {
std::to_string(currentPage + 1) + "/" + std::to_string(totalPages) + " " + std::to_string(progress) + "%"; char progressStr[32];
progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr.c_str()); if (showProgressPercentage) {
snprintf(progressStr, sizeof(progressStr), "%d/%d %.0f%%", currentPage + 1, totalPages, progress);
} else {
snprintf(progressStr, sizeof(progressStr), "%d/%d", currentPage + 1, totalPages);
}
progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr);
renderer.drawText(SMALL_FONT_ID, renderer.getScreenWidth() - orientedMarginRight - progressTextWidth, textY, renderer.drawText(SMALL_FONT_ID, renderer.getScreenWidth() - orientedMarginRight - progressTextWidth, textY,
progressStr.c_str()); progressStr);
}
if (showProgressBar) {
// Draw progress bar at the very bottom of the screen, from edge to edge of viewable area
ScreenComponents::drawBookProgressBar(renderer, static_cast<size_t>(progress));
} }
if (showBattery) { if (showBattery) {
ScreenComponents::drawBattery(renderer, orientedMarginLeft, textY); ScreenComponents::drawBattery(renderer, orientedMarginLeft, textY, showBatteryPercentage);
} }
if (showTitle) { if (showTitle) {

View File

@ -16,7 +16,8 @@ const SettingInfo displaySettings[displaySettingsCount] = {
// Should match with SLEEP_SCREEN_MODE // Should match with SLEEP_SCREEN_MODE
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}), SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}),
SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}), SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}),
SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}), SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar,
{"None", "No Progress", "Full w/ Percentage", "Full w/ Progress Bar", "Progress Bar"}),
SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}), SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}),
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})}; {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})};