@@ -0,0 +1,115 @@
|
||||
#include "EndOfBookOptions.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "ReaderUtils.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "util/ButtonNavigator.h"
|
||||
#include "util/NextBookFinder.h"
|
||||
|
||||
namespace {
|
||||
// Display name without the file extension, mirroring the file browser rows
|
||||
std::string displayName(const std::string& filename) {
|
||||
const auto pos = filename.rfind('.');
|
||||
return filename.substr(0, pos);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void EndOfBookOptions::loadOnce(const std::string& currentBookPath) {
|
||||
if (isLoaded.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
folder = FsHelpers::extractFolderPath(currentBookPath);
|
||||
names = NextBookFinder::findNextBooks(currentBookPath, MAX_SUGGESTIONS);
|
||||
selector = 0;
|
||||
// Release-publish so the main task, which gates all access on isLoaded, never
|
||||
// observes a partially built list
|
||||
isLoaded.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool EndOfBookOptions::menuActive() const { return isLoaded.load(std::memory_order_acquire) && !names.empty(); }
|
||||
|
||||
std::string EndOfBookOptions::fullPath(const size_t index) const {
|
||||
if (index >= names.size()) {
|
||||
return {};
|
||||
}
|
||||
return folder == "/" ? "/" + names[index] : folder + "/" + names[index];
|
||||
}
|
||||
|
||||
EndOfBookOptions::Action EndOfBookOptions::handleMenuInput(const MappedInputManager& input, std::string* openPath) {
|
||||
if (input.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (selector < static_cast<int>(names.size())) {
|
||||
if (openPath) {
|
||||
*openPath = fullPath(selector);
|
||||
}
|
||||
return Action::OpenBook;
|
||||
}
|
||||
return Action::GoHome; // "Home" entry selected
|
||||
}
|
||||
|
||||
// Short-press Back returns to the last page; a long press falls through to the
|
||||
// reader's own handler (file browser). Home is reached through the list's Home entry.
|
||||
if (input.wasReleased(MappedInputManager::Button::Back) && input.getHeldTime() < ReaderUtils::GO_HOME_MS) {
|
||||
return Action::LastPage;
|
||||
}
|
||||
|
||||
// Selection movement on the standard list navigation buttons (side Up/Down plus front
|
||||
// Left/Right, orientation swap included). It follows the reader's page-turn semantics
|
||||
// (press-triggered by default, release-triggered when a long-press behavior is
|
||||
// configured, same rule as ReaderUtils::detectPageTurn). This matters on entry: with
|
||||
// press-triggered turns, the press that turned the final page already fired in the
|
||||
// reader, and its release must not double-fire into this menu.
|
||||
const bool usePress = SETTINGS.longPressButtonBehavior == CrossPointSettings::OFF;
|
||||
const auto triggered = [&](const MappedInputManager::Button button) {
|
||||
return usePress ? input.wasPressed(button) : input.wasReleased(button);
|
||||
};
|
||||
const int itemCount = static_cast<int>(names.size()) + 1; // + "Home" entry
|
||||
if (triggered(MappedInputManager::Button::NavPrevious)) {
|
||||
selector = ButtonNavigator::previousIndex(selector, itemCount); // wraps to the bottom
|
||||
return Action::Redraw;
|
||||
}
|
||||
if (triggered(MappedInputManager::Button::NavNext)) {
|
||||
selector = ButtonNavigator::nextIndex(selector, itemCount); // wraps to the top
|
||||
return Action::Redraw;
|
||||
}
|
||||
return Action::None;
|
||||
}
|
||||
|
||||
void EndOfBookOptions::render(GfxRenderer& renderer, const MappedInputManager& input) const {
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
|
||||
if (!menuActive()) {
|
||||
// No suggestions: the historical plain end screen. 3/8 of the screen height matches
|
||||
// the previous fixed position on the 480x800 panel and scales to other resolutions.
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, renderer.getScreenHeight() * 3 / 8, tr(STR_END_OF_BOOK), true,
|
||||
EpdFontFamily::BOLD);
|
||||
return;
|
||||
}
|
||||
|
||||
// Suggestion menu: title, list (+ Home entry) and button hints. The hints are drawn at
|
||||
// the physical front buttons, which is a logical side/top edge in the rotated
|
||||
// orientations — lay out inside the safe area so nothing hides behind them. Vertical
|
||||
// positions derive from the safe-area height and font line heights so other panel
|
||||
// resolutions scale (review request on #2532).
|
||||
const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false);
|
||||
const int titleY = safe.y + safe.height / 8;
|
||||
const int subtitleY = titleY + renderer.getLineHeight(UI_12_FONT_ID) + metrics.verticalSpacing;
|
||||
const int listTop = subtitleY + renderer.getLineHeight(UI_10_FONT_ID) + metrics.verticalSpacing * 2;
|
||||
|
||||
UITheme::drawCenteredText(renderer, safe, UI_12_FONT_ID, titleY, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
|
||||
UITheme::drawCenteredText(renderer, safe, UI_10_FONT_ID, subtitleY, tr(STR_EOB_CONTINUE_WITH));
|
||||
|
||||
const int listHeight = safe.y + safe.height - listTop - metrics.verticalSpacing;
|
||||
GUI.drawList(renderer, Rect{safe.x, listTop, safe.width, listHeight}, static_cast<int>(names.size()) + 1, selector,
|
||||
[this](const int index) {
|
||||
return index < static_cast<int>(names.size()) ? displayName(names[index])
|
||||
: std::string(tr(STR_EOB_HOME));
|
||||
});
|
||||
|
||||
const auto labels = input.mapLabels(tr(STR_BACK), tr(STR_OPEN), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class GfxRenderer;
|
||||
class MappedInputManager;
|
||||
|
||||
// Shared End-of-Book next-book menu for the EPUB and XTC readers. Collects up to
|
||||
// MAX_SUGGESTIONS sibling books once per reader session, handles the menu input, and
|
||||
// draws the end screen. With no suggestions the end screen keeps its historical
|
||||
// plain-title look and behavior.
|
||||
class EndOfBookOptions {
|
||||
public:
|
||||
enum class Action { None, Redraw, OpenBook, GoHome, LastPage };
|
||||
|
||||
static constexpr size_t MAX_SUGGESTIONS = 3;
|
||||
|
||||
// Scans the book's folder for suggestions; no-op when already loaded. Call ONLY from
|
||||
// the reader's render() (the render task, serialized by RenderLock) — the loaded flag
|
||||
// is the release/acquire publication point that lets the main task read the finished
|
||||
// list safely.
|
||||
void loadOnce(const std::string& currentBookPath);
|
||||
|
||||
// True when the suggestion menu is showing and should own the reader's input.
|
||||
bool menuActive() const;
|
||||
|
||||
// Menu input handling, following the standard list idiom: side Up/Down and front
|
||||
// Left/Right move the selection (wrapping), Confirm opens it (or Home), and a short
|
||||
// Back press returns to the last page of the book. Fills openPath when the result is
|
||||
// OpenBook. Returns Action::None when nothing relevant was pressed; callers continue
|
||||
// their normal input path (keeping long-press Back to the file browser working).
|
||||
Action handleMenuInput(const MappedInputManager& input, std::string* openPath);
|
||||
|
||||
// Draws the full end screen (plain title, or the suggestion menu) onto a cleared buffer.
|
||||
void render(GfxRenderer& renderer, const MappedInputManager& input) const;
|
||||
|
||||
private:
|
||||
std::string folder;
|
||||
// Written by the render task in loadOnce(), immutable afterwards; the main task only
|
||||
// reads it after isLoaded is observed true (acquire), so no further locking is needed.
|
||||
std::vector<std::string> names;
|
||||
int selector = 0;
|
||||
std::atomic<bool> isLoaded{false};
|
||||
|
||||
std::string fullPath(size_t index) const;
|
||||
};
|
||||
@@ -231,6 +231,30 @@ void EpubReaderActivity::onExit() {
|
||||
}
|
||||
}
|
||||
|
||||
void EpubReaderActivity::openReaderMenu() {
|
||||
const int currentPage = section ? section->currentPage + 1 : 0;
|
||||
const int totalPages = section ? section->estimatedTotalPages() : 0;
|
||||
float bookProgress = 0.0f;
|
||||
if (epub->getBookSize() > 0 && section && section->estimatedTotalPages() > 0) {
|
||||
const float chapterProgress =
|
||||
static_cast<float>(section->currentPage) / static_cast<float>(section->estimatedTotalPages());
|
||||
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
||||
}
|
||||
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
||||
startActivityForResult(std::make_unique<EpubReaderMenuActivity>(
|
||||
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
||||
SETTINGS.orientation, !currentPageFootnotes.empty(), !cachedBookmarks.empty()),
|
||||
[this](const ActivityResult& result) {
|
||||
// Always apply orientation change even if the menu was cancelled
|
||||
const auto& menu = std::get<MenuResult>(result.data);
|
||||
applyOrientation(menu.orientation);
|
||||
toggleAutoPageTurn(menu.pageTurnOption);
|
||||
if (!result.isCancelled) {
|
||||
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void EpubReaderActivity::loop() {
|
||||
if (!epub) {
|
||||
// Should never happen
|
||||
@@ -325,6 +349,35 @@ void EpubReaderActivity::loop() {
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
// While the end screen suggestion menu is showing it owns Confirm/Back/navigation
|
||||
// input. Anything it doesn't handle (e.g. long-press Back to the file browser) falls
|
||||
// through to the regular handlers below; page turns are absorbed by the end-of-book
|
||||
// block. A Confirm release after a long-press function (bookmark/sync) fired is left
|
||||
// to the regular Confirm handler below, which consumes it via ignoreNextConfirmRelease.
|
||||
if (atEndOfBook && endOfBookOptions.menuActive() &&
|
||||
!(ignoreNextConfirmRelease && mappedInput.wasReleased(MappedInputManager::Button::Confirm))) {
|
||||
std::string openPath;
|
||||
switch (endOfBookOptions.handleMenuInput(mappedInput, &openPath)) {
|
||||
case EndOfBookOptions::Action::OpenBook:
|
||||
activityManager.goToReader(openPath);
|
||||
return;
|
||||
case EndOfBookOptions::Action::GoHome:
|
||||
onGoHome();
|
||||
return;
|
||||
case EndOfBookOptions::Action::LastPage:
|
||||
currentSpineIndex = std::max(epub->getSpineItemsCount() - 1, 0);
|
||||
nextPageNumber = 0;
|
||||
pendingPageJump = std::numeric_limits<uint16_t>::max();
|
||||
requestUpdate();
|
||||
return;
|
||||
case EndOfBookOptions::Action::Redraw:
|
||||
requestUpdate();
|
||||
return;
|
||||
case EndOfBookOptions::Action::None:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Enter reader menu activity on short-press Confirm. A long-press that fired a bound
|
||||
// function (bookmark or KOReader sync) sets ignoreNextConfirmRelease so the release
|
||||
// following the hold does not also open the menu.
|
||||
@@ -332,27 +385,7 @@ void EpubReaderActivity::loop() {
|
||||
if (ignoreNextConfirmRelease) {
|
||||
ignoreNextConfirmRelease = false;
|
||||
} else {
|
||||
const int currentPage = section ? section->currentPage + 1 : 0;
|
||||
const int totalPages = section ? section->estimatedTotalPages() : 0;
|
||||
float bookProgress = 0.0f;
|
||||
if (epub->getBookSize() > 0 && section && section->estimatedTotalPages() > 0) {
|
||||
const float chapterProgress =
|
||||
static_cast<float>(section->currentPage) / static_cast<float>(section->estimatedTotalPages());
|
||||
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
||||
}
|
||||
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
||||
startActivityForResult(std::make_unique<EpubReaderMenuActivity>(
|
||||
renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
||||
SETTINGS.orientation, !currentPageFootnotes.empty(), !cachedBookmarks.empty()),
|
||||
[this](const ActivityResult& result) {
|
||||
// Always apply orientation change even if the menu was cancelled
|
||||
const auto& menu = std::get<MenuResult>(result.data);
|
||||
applyOrientation(menu.orientation);
|
||||
toggleAutoPageTurn(menu.pageTurnOption);
|
||||
if (!result.isCancelled) {
|
||||
onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action));
|
||||
}
|
||||
});
|
||||
openReaderMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,8 +466,14 @@ void EpubReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// At end of the book, forward button goes home and back button returns to last page
|
||||
// At end of the book with no suggestion menu, forward button goes home and back
|
||||
// button returns to last page
|
||||
if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) {
|
||||
if (endOfBookOptions.menuActive()) {
|
||||
// Selection movement was handled above; absorb leftover page-turn triggers so
|
||||
// e.g. "previous" at the top of the list doesn't jump back into the book
|
||||
return;
|
||||
}
|
||||
if (nextTriggered) {
|
||||
onGoHome();
|
||||
} else {
|
||||
@@ -865,8 +904,11 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
|
||||
// Show end of book screen
|
||||
if (currentSpineIndex == epub->getSpineItemsCount()) {
|
||||
// Sole load site: runs on the render task (serialized by RenderLock); the main
|
||||
// task only reads the suggestions once the loaded flag is published
|
||||
endOfBookOptions.loadOnce(epub->getPath());
|
||||
renderer.clearScreen();
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
|
||||
endOfBookOptions.render(renderer, mappedInput);
|
||||
renderer.displayBuffer();
|
||||
automaticPageTurnActive = false;
|
||||
showPendingSyncSaveError();
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <optional>
|
||||
|
||||
#include "BookmarkEntry.h"
|
||||
#include "EndOfBookOptions.h"
|
||||
#include "EpubReaderMenuActivity.h"
|
||||
#include "ProgressMapper.h"
|
||||
#include "activities/Activity.h"
|
||||
@@ -45,6 +46,8 @@ class EpubReaderActivity final : public Activity {
|
||||
// Set when the reader is left at end-of-book and SETTINGS.moveFinishedToReadFolder is on.
|
||||
// Consumed in onExit() to relocate the finished book into /Read/.
|
||||
bool pendingReadFolderMove = false;
|
||||
// Next-book suggestion menu for the End-of-Book screen
|
||||
EndOfBookOptions endOfBookOptions;
|
||||
|
||||
// Footnote support
|
||||
std::vector<FootnoteEntry> currentPageFootnotes;
|
||||
@@ -88,6 +91,8 @@ class EpubReaderActivity final : public Activity {
|
||||
// Jump to a percentage of the book (0-100), mapping it to spine and page.
|
||||
void jumpToPercent(int percent);
|
||||
void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action);
|
||||
// Opens the reader menu for the current position (short-press Confirm)
|
||||
void openReaderMenu();
|
||||
// Returns true if sync acted (launched, or surfaced a save error); false if it was a no-op
|
||||
// because no KOReader credentials are stored.
|
||||
bool launchKOReaderSync();
|
||||
|
||||
@@ -53,18 +53,52 @@ void XtcReaderActivity::onExit() {
|
||||
xtc.reset();
|
||||
}
|
||||
|
||||
void XtcReaderActivity::openChapterSelection() {
|
||||
if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) {
|
||||
startActivityForResult(std::make_unique<XtcReaderChapterSelectionActivity>(renderer, mappedInput, xtc, currentPage),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
currentPage = std::get<PageResult>(result.data).page;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void XtcReaderActivity::loop() {
|
||||
if (!xtc) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool atEndOfBook = currentPage >= xtc->getPageCount();
|
||||
|
||||
// While the end screen suggestion menu is showing it owns Confirm/Back/navigation
|
||||
// input. Anything it doesn't handle (e.g. long-press Back to the file browser) falls
|
||||
// through to the regular handlers below; page turns are absorbed by the end-of-book
|
||||
// block.
|
||||
if (atEndOfBook && endOfBookOptions.menuActive()) {
|
||||
std::string openPath;
|
||||
switch (endOfBookOptions.handleMenuInput(mappedInput, &openPath)) {
|
||||
case EndOfBookOptions::Action::OpenBook:
|
||||
activityManager.goToReader(openPath);
|
||||
return;
|
||||
case EndOfBookOptions::Action::GoHome:
|
||||
onGoHome();
|
||||
return;
|
||||
case EndOfBookOptions::Action::LastPage:
|
||||
currentPage = xtc->getPageCount() > 0 ? xtc->getPageCount() - 1 : 0;
|
||||
requestUpdate();
|
||||
return;
|
||||
case EndOfBookOptions::Action::Redraw:
|
||||
requestUpdate();
|
||||
return;
|
||||
case EndOfBookOptions::Action::None:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Enter chapter selection activity
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) {
|
||||
startActivityForResult(
|
||||
std::make_unique<XtcReaderChapterSelectionActivity>(renderer, mappedInput, xtc, currentPage),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
currentPage = std::get<PageResult>(result.data).page;
|
||||
}
|
||||
});
|
||||
}
|
||||
openChapterSelection();
|
||||
}
|
||||
|
||||
// Long press BACK (1s+) goes to file selection
|
||||
@@ -85,8 +119,14 @@ void XtcReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// At end of the book, forward button goes home and back button returns to last page
|
||||
// At end of the book with no suggestion menu, forward button goes home and back
|
||||
// button returns to last page
|
||||
if (currentPage >= xtc->getPageCount()) {
|
||||
if (endOfBookOptions.menuActive()) {
|
||||
// Selection movement was handled above; absorb leftover page-turn triggers so
|
||||
// e.g. "previous" at the top of the list doesn't jump back into the book
|
||||
return;
|
||||
}
|
||||
if (nextTriggered) {
|
||||
onGoHome();
|
||||
} else {
|
||||
@@ -123,9 +163,11 @@ void XtcReaderActivity::render(RenderLock&&) {
|
||||
|
||||
// Bounds check
|
||||
if (currentPage >= xtc->getPageCount()) {
|
||||
// Show end of book screen
|
||||
// Show end of book screen. Sole load site: runs on the render task (serialized by
|
||||
// RenderLock); the main task only reads the suggestions once the flag is published.
|
||||
endOfBookOptions.loadOnce(xtc->getPath());
|
||||
renderer.clearScreen();
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
|
||||
endOfBookOptions.render(renderer, mappedInput);
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "EndOfBookOptions.h"
|
||||
#include "activities/Activity.h"
|
||||
|
||||
class XtcReaderActivity final : public Activity {
|
||||
@@ -19,6 +20,8 @@ class XtcReaderActivity final : public Activity {
|
||||
|
||||
uint32_t currentPage = 0;
|
||||
int pagesUntilFullRefresh = 0;
|
||||
// Next-book suggestion menu for the End-of-Book screen
|
||||
EndOfBookOptions endOfBookOptions;
|
||||
|
||||
enum class StatusBarOverlayPosition { Bottom, Top };
|
||||
struct StatusBarInfo {
|
||||
@@ -28,6 +31,8 @@ class XtcReaderActivity final : public Activity {
|
||||
};
|
||||
|
||||
void renderPage();
|
||||
// Opens chapter selection when the book has chapters (short-press Confirm); no-op otherwise
|
||||
void openChapterSelection();
|
||||
void renderStatusBarOverlay(StatusBarOverlayPosition position) const;
|
||||
StatusBarInfo getStatusBarInfo() const;
|
||||
void saveProgress() const;
|
||||
|
||||
Reference in New Issue
Block a user