mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 06:37:38 +03:00
feat: Add percent selection feature for jumping within the book
This commit is contained in:
parent
e5c0ddc9fa
commit
606b20d9d0
@ -5,12 +5,14 @@
|
||||
#include <GfxRenderer.h>
|
||||
#include <SDCardManager.h>
|
||||
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "EpubReaderChapterSelectionActivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "ScreenComponents.h"
|
||||
#include "activities/reader/EpubReaderPercentSelectionActivity.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
namespace {
|
||||
@ -22,6 +24,17 @@ constexpr int progressBarMarginTop = 1;
|
||||
|
||||
} // namespace
|
||||
|
||||
// Clamp any percent-like value into the valid 0-100 range.
|
||||
static int clampPercent(const int value) {
|
||||
if (value < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (value > 100) {
|
||||
return 100;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void EpubReaderActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<EpubReaderActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
@ -123,7 +136,19 @@ void EpubReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter chapter selection activity
|
||||
// Enter reader menu activity (suppressed after slider confirm/cancel).
|
||||
if (suppressMenuOpenOnce) {
|
||||
// If we're seeing the confirm release that closed the slider, consume it and return.
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
suppressMenuOpenOnce = false;
|
||||
return;
|
||||
}
|
||||
// If confirm is no longer pressed and no release is pending, clear suppression.
|
||||
if (!mappedInput.isPressed(MappedInputManager::Button::Confirm)) {
|
||||
suppressMenuOpenOnce = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
// Don't start activity transition while rendering
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
@ -225,6 +250,85 @@ void EpubReaderActivity::onReaderMenuBack() {
|
||||
updateRequired = true;
|
||||
}
|
||||
|
||||
// Translate an absolute percent into a spine index plus a normalized position
|
||||
// within that spine so we can jump after the section is loaded.
|
||||
void EpubReaderActivity::jumpToPercent(int percent) {
|
||||
if (!epub) {
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t bookSize = epub->getBookSize();
|
||||
if (bookSize == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize input to 0-100 to avoid invalid jumps.
|
||||
percent = clampPercent(percent);
|
||||
|
||||
// Convert percent into a byte-like absolute position across the spine sizes.
|
||||
size_t targetSize = (bookSize * static_cast<size_t>(percent)) / 100;
|
||||
if (percent >= 100 && bookSize > 0) {
|
||||
// Ensure the final percent lands inside the last spine item.
|
||||
targetSize = bookSize - 1;
|
||||
}
|
||||
|
||||
const int spineCount = epub->getSpineItemsCount();
|
||||
if (spineCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int targetSpineIndex = spineCount - 1;
|
||||
size_t prevCumulative = 0;
|
||||
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
const size_t cumulative = epub->getCumulativeSpineItemSize(i);
|
||||
if (targetSize <= cumulative) {
|
||||
// Found the spine item containing the absolute position.
|
||||
targetSpineIndex = i;
|
||||
prevCumulative = (i > 0) ? epub->getCumulativeSpineItemSize(i - 1) : 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const size_t cumulative = epub->getCumulativeSpineItemSize(targetSpineIndex);
|
||||
const size_t spineSize = (cumulative > prevCumulative) ? (cumulative - prevCumulative) : 0;
|
||||
// Store a normalized position within the spine so it can be applied once loaded.
|
||||
pendingSpineProgress = (spineSize == 0) ? 0.0f
|
||||
: static_cast<float>(targetSize - prevCumulative) /
|
||||
static_cast<float>(spineSize);
|
||||
if (pendingSpineProgress < 0.0f) {
|
||||
pendingSpineProgress = 0.0f;
|
||||
} else if (pendingSpineProgress > 1.0f) {
|
||||
pendingSpineProgress = 1.0f;
|
||||
}
|
||||
|
||||
// Reset state so renderScreen() reloads and repositions on the target spine.
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
currentSpineIndex = targetSpineIndex;
|
||||
nextPageNumber = 0;
|
||||
pendingPercentJump = true;
|
||||
section.reset();
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
|
||||
// Compute the overall reading position as a percent of the book.
|
||||
int EpubReaderActivity::getCurrentPercent() const {
|
||||
if (!epub || epub->getBookSize() == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Estimate within-spine progress based on the current page.
|
||||
float chapterProgress = 0.0f;
|
||||
if (section && section->pageCount > 0) {
|
||||
chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
|
||||
}
|
||||
|
||||
// Convert to overall progress using cumulative spine sizes.
|
||||
const float progress = epub->calculateProgress(currentSpineIndex, chapterProgress);
|
||||
const int percent = static_cast<int>(progress * 100.0f + 0.5f);
|
||||
return clampPercent(percent);
|
||||
}
|
||||
|
||||
void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) {
|
||||
switch (action) {
|
||||
case EpubReaderMenuActivity::MenuAction::SELECT_CHAPTER: {
|
||||
@ -268,6 +372,29 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
||||
xSemaphoreGive(renderingMutex);
|
||||
break;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::GO_TO_PERCENT: {
|
||||
// Launch the slider-based percent selector and return here on confirm/cancel.
|
||||
const int initialPercent = getCurrentPercent();
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
exitActivity();
|
||||
enterNewActivity(new EpubReaderPercentSelectionActivity(
|
||||
renderer, mappedInput, initialPercent,
|
||||
[this](const int percent) {
|
||||
// Apply the new position and exit back to the reader.
|
||||
jumpToPercent(percent);
|
||||
suppressMenuOpenOnce = true;
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
},
|
||||
[this]() {
|
||||
// Cancel selection and return to the reader.
|
||||
suppressMenuOpenOnce = true;
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
}));
|
||||
xSemaphoreGive(renderingMutex);
|
||||
break;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::GO_HOME: {
|
||||
// 2. Trigger the reader's "Go Home" callback
|
||||
if (onGoHome) {
|
||||
@ -398,6 +525,18 @@ void EpubReaderActivity::renderScreen() {
|
||||
}
|
||||
cachedChapterTotalPageCount = 0; // resets to 0 to prevent reading cached progress again
|
||||
}
|
||||
|
||||
if (pendingPercentJump && section->pageCount > 0) {
|
||||
// Apply the pending percent jump now that we know the new section's page count.
|
||||
int newPage = static_cast<int>(pendingSpineProgress * static_cast<float>(section->pageCount));
|
||||
if (newPage < 0) {
|
||||
newPage = 0;
|
||||
} else if (newPage >= section->pageCount) {
|
||||
newPage = section->pageCount - 1;
|
||||
}
|
||||
section->currentPage = newPage;
|
||||
pendingPercentJump = false;
|
||||
}
|
||||
}
|
||||
|
||||
renderer.clearScreen();
|
||||
|
||||
@ -18,6 +18,13 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
|
||||
int pagesUntilFullRefresh = 0;
|
||||
int cachedSpineIndex = 0;
|
||||
int cachedChapterTotalPageCount = 0;
|
||||
// Signals that the next render should reposition within the newly loaded section
|
||||
// based on a cross-book percentage jump.
|
||||
bool pendingPercentJump = false;
|
||||
// Normalized 0.0-1.0 progress within the target spine item, computed from book percentage.
|
||||
float pendingSpineProgress = 0.0f;
|
||||
// Prevents the reader menu from reopening due to the confirm button used to exit the slider.
|
||||
bool suppressMenuOpenOnce = false;
|
||||
bool updateRequired = false;
|
||||
const std::function<void()> onGoBack;
|
||||
const std::function<void()> onGoHome;
|
||||
@ -29,6 +36,10 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
|
||||
int orientedMarginBottom, int orientedMarginLeft);
|
||||
void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const;
|
||||
void saveProgress(int spineIndex, int currentPage, int pageCount);
|
||||
// Jump to a percentage of the book (0-100), mapping it to spine and page.
|
||||
void jumpToPercent(int percent);
|
||||
// Compute the current reading position as an integer percent (0-100).
|
||||
int getCurrentPercent() const;
|
||||
void onReaderMenuBack();
|
||||
void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action);
|
||||
|
||||
|
||||
@ -13,7 +13,8 @@
|
||||
|
||||
class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
||||
public:
|
||||
enum class MenuAction { SELECT_CHAPTER, GO_HOME, DELETE_CACHE };
|
||||
// Menu actions available from the reader menu.
|
||||
enum class MenuAction { SELECT_CHAPTER, GO_TO_PERCENT, GO_HOME, DELETE_CACHE };
|
||||
|
||||
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
|
||||
const std::function<void()>& onBack, const std::function<void(MenuAction)>& onAction)
|
||||
@ -32,7 +33,9 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
||||
std::string label;
|
||||
};
|
||||
|
||||
// Fixed menu layout (order matters for up/down navigation).
|
||||
const std::vector<MenuItem> menuItems = {{MenuAction::SELECT_CHAPTER, "Go to Chapter"},
|
||||
{MenuAction::GO_TO_PERCENT, "Go to %"},
|
||||
{MenuAction::GO_HOME, "Go Home"},
|
||||
{MenuAction::DELETE_CACHE, "Delete Book Cache"}};
|
||||
|
||||
|
||||
138
src/activities/reader/EpubReaderPercentSelectionActivity.cpp
Normal file
138
src/activities/reader/EpubReaderPercentSelectionActivity.cpp
Normal file
@ -0,0 +1,138 @@
|
||||
#include "EpubReaderPercentSelectionActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
namespace {
|
||||
// Fine/coarse slider step sizes for percent adjustments.
|
||||
constexpr int kSmallStep = 1;
|
||||
constexpr int kLargeStep = 10;
|
||||
}
|
||||
|
||||
void EpubReaderPercentSelectionActivity::onEnter() {
|
||||
ActivityWithSubactivity::onEnter();
|
||||
// Set up rendering task and mark first frame dirty.
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
updateRequired = true;
|
||||
xTaskCreate(&EpubReaderPercentSelectionActivity::taskTrampoline, "EpubPercentSlider", 4096, this, 1,
|
||||
&displayTaskHandle);
|
||||
}
|
||||
|
||||
void EpubReaderPercentSelectionActivity::onExit() {
|
||||
ActivityWithSubactivity::onExit();
|
||||
// Ensure the render task is stopped before freeing the mutex.
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
vTaskDelete(displayTaskHandle);
|
||||
displayTaskHandle = nullptr;
|
||||
}
|
||||
vSemaphoreDelete(renderingMutex);
|
||||
renderingMutex = nullptr;
|
||||
}
|
||||
|
||||
void EpubReaderPercentSelectionActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<EpubReaderPercentSelectionActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void EpubReaderPercentSelectionActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
// Render only when the view is dirty and no subactivity is running.
|
||||
if (updateRequired && !subActivity) {
|
||||
updateRequired = false;
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
renderScreen();
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void EpubReaderPercentSelectionActivity::adjustPercent(const int delta) {
|
||||
// Apply delta and clamp within 0-100.
|
||||
percent += delta;
|
||||
if (percent < 0) {
|
||||
percent = 0;
|
||||
} else if (percent > 100) {
|
||||
percent = 100;
|
||||
}
|
||||
updateRequired = true;
|
||||
}
|
||||
|
||||
void EpubReaderPercentSelectionActivity::loop() {
|
||||
if (subActivity) {
|
||||
subActivity->loop();
|
||||
return;
|
||||
}
|
||||
|
||||
// Back cancels, confirm selects, arrows adjust the percent.
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
onSelect(percent);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Left)) {
|
||||
adjustPercent(-kSmallStep);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
||||
adjustPercent(kSmallStep);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Up)) {
|
||||
adjustPercent(kLargeStep);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Down)) {
|
||||
adjustPercent(-kLargeStep);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void EpubReaderPercentSelectionActivity::renderScreen() {
|
||||
renderer.clearScreen();
|
||||
|
||||
// Title and numeric percent value.
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Go to Position", true, EpdFontFamily::BOLD);
|
||||
|
||||
const std::string percentText = std::to_string(percent) + "%";
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 70, percentText.c_str(), true, EpdFontFamily::BOLD);
|
||||
|
||||
// Draw slider track.
|
||||
const int screenWidth = renderer.getScreenWidth();
|
||||
constexpr int barWidth = 360;
|
||||
constexpr int barHeight = 16;
|
||||
const int barX = (screenWidth - barWidth) / 2;
|
||||
const int barY = 120;
|
||||
|
||||
renderer.drawRect(barX, barY, barWidth, barHeight);
|
||||
|
||||
// Fill slider based on percent.
|
||||
const int fillWidth = (barWidth - 4) * percent / 100;
|
||||
if (fillWidth > 0) {
|
||||
renderer.fillRect(barX + 2, barY + 2, fillWidth, barHeight - 4);
|
||||
}
|
||||
|
||||
// Draw a simple knob centered at the current percent.
|
||||
const int knobX = barX + 2 + fillWidth - 2;
|
||||
renderer.fillRect(knobX, barY - 4, 4, barHeight + 8, true);
|
||||
|
||||
// Hint text for step sizes.
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, barY + 30, "Left/Right: 1% Up/Down: 10%", true);
|
||||
|
||||
// Button hints follow the current front button layout.
|
||||
const auto labels = mappedInput.mapLabels("« Back", "Select", "-", "+");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
46
src/activities/reader/EpubReaderPercentSelectionActivity.h
Normal file
46
src/activities/reader/EpubReaderPercentSelectionActivity.h
Normal file
@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "activities/ActivityWithSubactivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
|
||||
class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity {
|
||||
public:
|
||||
// Slider-style percent selector for jumping within a book.
|
||||
explicit EpubReaderPercentSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const int initialPercent, const std::function<void(int)>& onSelect,
|
||||
const std::function<void()>& onCancel)
|
||||
: ActivityWithSubactivity("EpubReaderPercentSelection", renderer, mappedInput),
|
||||
percent(initialPercent),
|
||||
onSelect(onSelect),
|
||||
onCancel(onCancel) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
|
||||
private:
|
||||
// Current percent value (0-100) shown on the slider.
|
||||
int percent = 0;
|
||||
// Render dirty flag for the task loop.
|
||||
bool updateRequired = false;
|
||||
// FreeRTOS task and mutex for rendering.
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
|
||||
// Callback invoked when the user confirms a percent.
|
||||
const std::function<void(int)> onSelect;
|
||||
// Callback invoked when the user cancels the slider.
|
||||
const std::function<void()> onCancel;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
// Render the slider UI.
|
||||
void renderScreen();
|
||||
// Change the current percent by a delta and clamp within bounds.
|
||||
void adjustPercent(int delta);
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user