Merge pull request #96 from jpirnay/examine-memory-regression-master
refactor: fix memory regression due to Activity push
This commit is contained in:
@@ -11,6 +11,7 @@ This document explains the refactoring from the original per-activity render tas
|
|||||||
| `RenderLock` | Inner class of `Activity` | Standalone class, acquires global mutex |
|
| `RenderLock` | Inner class of `Activity` | Standalone class, acquires global mutex |
|
||||||
| Subactivities | `ActivityWithSubactivity` base class | Activity stack managed by `ActivityManager` |
|
| Subactivities | `ActivityWithSubactivity` base class | Activity stack managed by `ActivityManager` |
|
||||||
| Navigation | Free functions in `main.cpp` | `activityManager.goHome()`, `goToReader()`, etc. |
|
| Navigation | Free functions in `main.cpp` | `activityManager.goHome()`, `goToReader()`, etc. |
|
||||||
|
| Forward flow | Parent stays on stack (`pushActivity`) | Parent destroyed on forward flow (`replaceWith*` + `ReturnHint`) |
|
||||||
| Subactivity results | Callback lambdas stored in parent | `startActivityForResult()` / `setResult()` / `finish()` |
|
| Subactivity results | Callback lambdas stored in parent | `startActivityForResult()` / `setResult()` / `finish()` |
|
||||||
| `requestUpdate()` | Notifies activity's own render task | Delegates to `ActivityManager` (immediate or deferred) |
|
| `requestUpdate()` | Notifies activity's own render task | Delegates to `ActivityManager` (immediate or deferred) |
|
||||||
|
|
||||||
@@ -99,7 +100,7 @@ class MyActivity final : public Activity {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that navigation callbacks like `goBack` are no longer stored — use `finish()` or `activityManager.goHome()` instead.
|
Note that navigation callbacks like `goBack` are no longer stored — use `finish()`, `onGoHome()`, or a direct `activityManager.goHome()` / `goTo*()` / `replaceWith*()` call instead.
|
||||||
|
|
||||||
### 2. Replace Navigation Functions
|
### 2. Replace Navigation Functions
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ activityManager.goToSettings();
|
|||||||
activityManager.replaceActivity(std::make_unique<MyActivity>(renderer, mappedInput));
|
activityManager.replaceActivity(std::make_unique<MyActivity>(renderer, mappedInput));
|
||||||
```
|
```
|
||||||
|
|
||||||
`replaceActivity()` destroys the current activity and clears the stack. Use it for top-level navigation (home, reader, settings, etc.).
|
`replaceActivity()` destroys the current activity and clears the stack. Use it for top-level navigation (home, reader, settings, etc.). See the "Navigation Flow" section after the checklist for when to use replace vs push, and how the `ReturnHint` mechanism restores a parent's prior state without keeping it resident.
|
||||||
|
|
||||||
### 3. Replace Subactivity Pattern
|
### 3. Replace Subactivity Pattern
|
||||||
|
|
||||||
@@ -234,12 +235,82 @@ class SettingsActivity : public Activity {
|
|||||||
public:
|
public:
|
||||||
SettingsActivity(GfxRenderer& r, MappedInputManager& m)
|
SettingsActivity(GfxRenderer& r, MappedInputManager& m)
|
||||||
: Activity("Settings", r, m) {}
|
: Activity("Settings", r, m) {}
|
||||||
// Use finish() to go back, activityManager.goHome() to go home
|
// Use finish() to go back (pops the stack, or returns via ReturnHint if empty),
|
||||||
|
// onGoHome() to exit the flow ("up and out"), or activityManager.goHome() for a hard reset.
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
This removes `std::function` overhead (~2-4KB per unique signature) and eliminates lifetime risks from captured `this` pointers.
|
This removes `std::function` overhead (~2-4KB per unique signature) and eliminates lifetime risks from captured `this` pointers.
|
||||||
|
|
||||||
|
## Navigation Flow
|
||||||
|
|
||||||
|
### Push vs Replace: Memory Matters
|
||||||
|
|
||||||
|
On an ESP32-C3, heap fragmentation is a real constraint — especially when the next activity is heavy (EPUB reader, TLS sync). There are two ways to launch a new activity, and the choice matters:
|
||||||
|
|
||||||
|
| Call | Current activity | Stack | When to use |
|
||||||
|
|----------------------------------------------------|---------------------------------|------------------|-------------------------------------------------------------|
|
||||||
|
| `replaceActivity()` / `goTo*()` / `replaceWith*()` | **destroyed** (onExit + delete) | cleared | Forward flow: the caller has no reason to stay resident |
|
||||||
|
| `pushActivity()` / `startActivityForResult()` | kept alive on stack | parent preserved | Modal result flow: the caller needs to resume with a result |
|
||||||
|
|
||||||
|
**Default to replace.** Push is only correct when you need to deliver a result back to a still-living parent (keyboard entry, confirmation dialog, chapter picker, etc.). For plain one-way transitions — opening a book, going to settings, switching tabs — use replace so the parent's memory is freed before the next activity runs.
|
||||||
|
|
||||||
|
### The `ReturnHint` Pattern
|
||||||
|
|
||||||
|
Replace destroys the parent, but the user still expects Back to return "where they came from" — not always Home. To bridge that, `ActivityManager` holds a small `ReturnHint`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
enum class ReturnTo : uint8_t { Home, FileBrowser, RecentBooks };
|
||||||
|
|
||||||
|
struct ReturnHint {
|
||||||
|
ReturnTo target = ReturnTo::Home;
|
||||||
|
std::string path; // FileBrowser directory to restore
|
||||||
|
std::string selectName; // item to re-focus (file name, book path)
|
||||||
|
int selectIndex = -1; // combined-list index (Home selector, Recents row)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
A parent records a hint before launching a forward flow. When the launched activity (or anything it chains to) eventually exits with an empty stack, `ActivityManager::returnFromChild()` consumes the hint and routes to the correct parent — restoring its prior selection. If no hint is set, it falls back to `goHome()`.
|
||||||
|
|
||||||
|
Two ways to set a hint:
|
||||||
|
|
||||||
|
**1. Dedicated wrappers** — for the common book-open paths:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// FileBrowserActivity::onFileOpen
|
||||||
|
ReturnHint hint;
|
||||||
|
hint.target = ReturnTo::FileBrowser;
|
||||||
|
hint.path = basepath; // "/books/fiction"
|
||||||
|
hint.selectName = entry; // "war_and_peace.epub"
|
||||||
|
activityManager.replaceWithReader(fullPath, std::move(hint));
|
||||||
|
|
||||||
|
// RecentBooksActivity::onSelect
|
||||||
|
ReturnHint hint;
|
||||||
|
hint.target = ReturnTo::RecentBooks;
|
||||||
|
hint.selectIndex = selectorIndex;
|
||||||
|
activityManager.replaceWithReader(path, std::move(hint));
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. `setReturnHint()` + any `goTo*()`** — for arbitrary transitions where a dedicated wrapper would be overkill:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// HomeActivity::dispatchMenuAction
|
||||||
|
ReturnHint hint;
|
||||||
|
hint.target = ReturnTo::Home;
|
||||||
|
hint.selectIndex = selectorIndex; // restore focus on the same menu entry
|
||||||
|
activityManager.setReturnHint(std::move(hint));
|
||||||
|
activityManager.goToSettings(); // parent destroyed; hint survives the round trip
|
||||||
|
```
|
||||||
|
|
||||||
|
`goTo*()` helpers do **not** clear the hint — only `goHome()` (explicit hard-reset) and the `replaceWith*()` helpers (which overwrite it with their own hint) do. This lets a hint survive chained transitions: Home → Reader → KOReaderSync → Reader → back to Home, hint intact.
|
||||||
|
|
||||||
|
How `finish()` interacts with the hint:
|
||||||
|
|
||||||
|
- **Non-empty stack**: `finish()` pops to the parent on the stack (classic modal result flow). Hint is untouched.
|
||||||
|
- **Empty stack**: `finish()` falls through to `returnFromChild()` automatically. An activity launched via a `replaceWith*()` helper has no stack — so its Back-button `finish()` naturally routes via the hint.
|
||||||
|
|
||||||
|
`onGoHome()` is now semantically "up and out" — it calls `returnFromChild()`, so long-press Back in a reader returns to whichever view opened the book, not always Home. For an explicit hard-reset, call `activityManager.goHome()` directly.
|
||||||
|
|
||||||
## Technical Details
|
## Technical Details
|
||||||
|
|
||||||
### FreeRTOS Task Model
|
### FreeRTOS Task Model
|
||||||
@@ -444,6 +515,10 @@ Child calls: setResult(MyResult{...}); finish();
|
|||||||
|
|
||||||
**Creating background tasks that outlive the activity**: Any FreeRTOS task created in `onEnter()` must be deleted in `onExit()` before the activity is destroyed. The `ActivityManager` does not track or clean up background tasks.
|
**Creating background tasks that outlive the activity**: Any FreeRTOS task created in `onEnter()` must be deleted in `onExit()` before the activity is destroyed. The `ActivityManager` does not track or clean up background tasks.
|
||||||
|
|
||||||
|
**Using push for forward navigation**: `pushActivity()` / `startActivityForResult()` keeps the parent alive on the stack. For a heavy child (EPUB reader, TLS sync) on a fragmented heap, the parent's resident allocations can be the difference between a successful launch and OOM. Only push when you need the parent to receive a result — otherwise use `replaceActivity()` / `goTo*()` / `replaceWith*()` and let the parent be freed first. If you do need "back to where I came from" semantics, record a `ReturnHint` before the replace instead of pushing.
|
||||||
|
|
||||||
|
**Stale `ReturnHint`**: A hint set by one activity persists until either `returnFromChild()` / `goHome()` clears it, or a `replaceWith*()` helper overwrites it. If you record a hint but the flow aborts down an unusual path (error screen, boot transition), the next unrelated `finish()` could consume it. Prefer setting the hint immediately before the transition, and call `activityManager.clearReturnHint()` if you abort the flow without launching the intended target.
|
||||||
|
|
||||||
**Holding `RenderLock` across blocking calls**: The render task is blocked on the mutex while you hold the lock. Keep critical sections short — acquire, mutate state, release, then do blocking work.
|
**Holding `RenderLock` across blocking calls**: The render task is blocked on the mutex while you hold the lock. Keep critical sections short — acquire, mutate state, release, then do blocking work.
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
|
|||||||
+8
-20
@@ -34,21 +34,13 @@
|
|||||||
// deep inside the heap allocator chain — enough stack to overflow the 8 KB loop task stack
|
// deep inside the heap allocator chain — enough stack to overflow the 8 KB loop task stack
|
||||||
// when called from inside SETTINGS.loadFromFile() at boot time.
|
// when called from inside SETTINGS.loadFromFile() at boot time.
|
||||||
namespace SettingsListDetail {
|
namespace SettingsListDetail {
|
||||||
inline uint8_t getKoReaderMatchMethod(const void*) {
|
inline uint8_t getKoReaderMatchMethod(const void*) { return static_cast<uint8_t>(KOREADER_STORE.getMatchMethod()); }
|
||||||
return static_cast<uint8_t>(KOREADER_STORE.getMatchMethod());
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::string getKoReaderServerUrl(void*) {
|
inline std::string getKoReaderServerUrl(void*) { return KOREADER_STORE.getServerUrl(); }
|
||||||
return KOREADER_STORE.getServerUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::string getKoReaderUsername(void*) {
|
inline std::string getKoReaderUsername(void*) { return KOREADER_STORE.getUsername(); }
|
||||||
return KOREADER_STORE.getUsername();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::string getKoReaderPassword(void*) {
|
inline std::string getKoReaderPassword(void*) { return KOREADER_STORE.getPassword(); }
|
||||||
return KOREADER_STORE.getPassword();
|
|
||||||
}
|
|
||||||
|
|
||||||
inline const std::vector<SettingInfo> list = {
|
inline const std::vector<SettingInfo> list = {
|
||||||
// --- Display ---
|
// --- Display ---
|
||||||
@@ -167,24 +159,21 @@ inline const std::vector<SettingInfo> list = {
|
|||||||
|
|
||||||
// --- KOReader Sync (web-only, uses KOReaderCredentialStore) ---
|
// --- KOReader Sync (web-only, uses KOReaderCredentialStore) ---
|
||||||
SettingInfo::DynamicString(
|
SettingInfo::DynamicString(
|
||||||
StrId::STR_SYNC_SERVER_URL,
|
StrId::STR_SYNC_SERVER_URL, static_cast<SettingInfo::StringGetterFn>(getKoReaderServerUrl),
|
||||||
static_cast<SettingInfo::StringGetterFn>(getKoReaderServerUrl),
|
|
||||||
[](void*, const std::string& v) {
|
[](void*, const std::string& v) {
|
||||||
KOREADER_STORE.setServerUrl(v);
|
KOREADER_STORE.setServerUrl(v);
|
||||||
KOREADER_STORE.saveToFile();
|
KOREADER_STORE.saveToFile();
|
||||||
},
|
},
|
||||||
"koServerUrl", StrId::STR_KOREADER_SYNC),
|
"koServerUrl", StrId::STR_KOREADER_SYNC),
|
||||||
SettingInfo::DynamicString(
|
SettingInfo::DynamicString(
|
||||||
StrId::STR_KOREADER_USERNAME,
|
StrId::STR_KOREADER_USERNAME, static_cast<SettingInfo::StringGetterFn>(getKoReaderUsername),
|
||||||
static_cast<SettingInfo::StringGetterFn>(getKoReaderUsername),
|
|
||||||
[](void*, const std::string& v) {
|
[](void*, const std::string& v) {
|
||||||
KOREADER_STORE.setCredentials(v, KOREADER_STORE.getPassword());
|
KOREADER_STORE.setCredentials(v, KOREADER_STORE.getPassword());
|
||||||
KOREADER_STORE.saveToFile();
|
KOREADER_STORE.saveToFile();
|
||||||
},
|
},
|
||||||
"koUsername", StrId::STR_KOREADER_SYNC),
|
"koUsername", StrId::STR_KOREADER_SYNC),
|
||||||
SettingInfo::DynamicString(
|
SettingInfo::DynamicString(
|
||||||
StrId::STR_KOREADER_PASSWORD,
|
StrId::STR_KOREADER_PASSWORD, static_cast<SettingInfo::StringGetterFn>(getKoReaderPassword),
|
||||||
static_cast<SettingInfo::StringGetterFn>(getKoReaderPassword),
|
|
||||||
[](void*, const std::string& v) {
|
[](void*, const std::string& v) {
|
||||||
KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), v);
|
KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), v);
|
||||||
KOREADER_STORE.saveToFile();
|
KOREADER_STORE.saveToFile();
|
||||||
@@ -192,8 +181,7 @@ inline const std::vector<SettingInfo> list = {
|
|||||||
"koPassword", StrId::STR_KOREADER_SYNC)
|
"koPassword", StrId::STR_KOREADER_SYNC)
|
||||||
.withObfuscated(),
|
.withObfuscated(),
|
||||||
SettingInfo::DynamicEnum(
|
SettingInfo::DynamicEnum(
|
||||||
StrId::STR_DOCUMENT_MATCHING, {StrId::STR_FILENAME, StrId::STR_BINARY},
|
StrId::STR_DOCUMENT_MATCHING, {StrId::STR_FILENAME, StrId::STR_BINARY}, getKoReaderMatchMethod,
|
||||||
getKoReaderMatchMethod,
|
|
||||||
[](void*, uint8_t v) {
|
[](void*, uint8_t v) {
|
||||||
KOREADER_STORE.setMatchMethod(static_cast<DocumentMatchMethod>(v));
|
KOREADER_STORE.setMatchMethod(static_cast<DocumentMatchMethod>(v));
|
||||||
KOREADER_STORE.saveToFile();
|
KOREADER_STORE.saveToFile();
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ void Activity::requestUpdate(bool immediate) { activityManager.requestUpdate(imm
|
|||||||
|
|
||||||
void Activity::requestUpdateAndWait() { activityManager.requestUpdateAndWait(); }
|
void Activity::requestUpdateAndWait() { activityManager.requestUpdateAndWait(); }
|
||||||
|
|
||||||
void Activity::onGoHome() { activityManager.goHome(); }
|
// "Up and out" — return to whichever parent launched this flow. If no return hint
|
||||||
|
// is set (typical for activities launched via a plain goTo*()), falls back to Home.
|
||||||
void Activity::onSelectBook(const std::string& path) { activityManager.pushReader(path); }
|
void Activity::onGoHome() { activityManager.returnFromChild(); }
|
||||||
|
|
||||||
void Activity::startActivityForResult(std::unique_ptr<Activity>&& activity, ActivityResultHandler resultHandler) {
|
void Activity::startActivityForResult(std::unique_ptr<Activity>&& activity, ActivityResultHandler resultHandler) {
|
||||||
this->resultHandler = std::move(resultHandler);
|
this->resultHandler = std::move(resultHandler);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class Activity {
|
|||||||
explicit Activity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput)
|
explicit Activity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||||
: name(std::move(name)), renderer(renderer), mappedInput(mappedInput) {}
|
: name(std::move(name)), renderer(renderer), mappedInput(mappedInput) {}
|
||||||
virtual ~Activity() = default;
|
virtual ~Activity() = default;
|
||||||
|
const std::string& getName() const { return name; }
|
||||||
virtual void onEnter();
|
virtual void onEnter();
|
||||||
virtual void onExit();
|
virtual void onExit();
|
||||||
virtual void loop() {}
|
virtual void loop() {}
|
||||||
@@ -57,5 +58,4 @@ class Activity {
|
|||||||
// Convenience method to facilitate API transition to ActivityManager
|
// Convenience method to facilitate API transition to ActivityManager
|
||||||
// TODO: remove this in near future
|
// TODO: remove this in near future
|
||||||
void onGoHome();
|
void onGoHome();
|
||||||
void onSelectBook(const std::string& path);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <HalClock.h>
|
#include <HalClock.h>
|
||||||
#include <HalPowerManager.h>
|
#include <HalPowerManager.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <esp_heap_caps.h>
|
||||||
|
#include <esp_system.h>
|
||||||
|
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
#include "boot_sleep/BootActivity.h"
|
#include "boot_sleep/BootActivity.h"
|
||||||
@@ -19,6 +22,10 @@
|
|||||||
#include "util/FullScreenMessageActivity.h"
|
#include "util/FullScreenMessageActivity.h"
|
||||||
#include "weather/WeatherActivity.h"
|
#include "weather/WeatherActivity.h"
|
||||||
|
|
||||||
|
#ifndef DEBUG_MEMORY_CONSUMPTION
|
||||||
|
#define DEBUG_MEMORY_CONSUMPTION 0
|
||||||
|
#endif
|
||||||
|
|
||||||
void ActivityManager::begin() {
|
void ActivityManager::begin() {
|
||||||
xTaskCreate(&renderTaskTrampoline, "ActivityManagerRender",
|
xTaskCreate(&renderTaskTrampoline, "ActivityManagerRender",
|
||||||
8192, // Stack size
|
8192, // Stack size
|
||||||
@@ -29,6 +36,17 @@ void ActivityManager::begin() {
|
|||||||
assert(renderTaskHandle != nullptr && "Failed to create render task");
|
assert(renderTaskHandle != nullptr && "Failed to create render task");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG_MEMORY_CONSUMPTION
|
||||||
|
static void logActivityStackState(const char* stage, Activity* currentActivity, size_t stackSize) {
|
||||||
|
const uint32_t freeHeap = esp_get_free_heap_size();
|
||||||
|
const uint32_t contigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||||
|
LOG_DBG("ACT", "%s: current=%s stackSize=%zu free=%lu contig=%lu", stage,
|
||||||
|
currentActivity ? currentActivity->getName().c_str() : "<none>", stackSize, freeHeap, contigHeap);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
static inline void logActivityStackState(const char*, Activity*, size_t) {}
|
||||||
|
#endif
|
||||||
|
|
||||||
void ActivityManager::renderTaskTrampoline(void* param) {
|
void ActivityManager::renderTaskTrampoline(void* param) {
|
||||||
auto* self = static_cast<ActivityManager*>(param);
|
auto* self = static_cast<ActivityManager*>(param);
|
||||||
self->renderTaskLoop();
|
self->renderTaskLoop();
|
||||||
@@ -111,10 +129,10 @@ void ActivityManager::loop() {
|
|||||||
pendingAction = PendingAction::None;
|
pendingAction = PendingAction::None;
|
||||||
|
|
||||||
if (stackActivities.empty()) {
|
if (stackActivities.empty()) {
|
||||||
LOG_DBG("ACT", "No more activities on stack, going home");
|
LOG_DBG("ACT", "No more activities on stack, returning from child");
|
||||||
lock.unlock(); // goHome may acquire its own lock
|
lock.unlock(); // returnFromChild may acquire its own lock via replaceActivity
|
||||||
goHome();
|
returnFromChild();
|
||||||
continue; // Will launch goHome immediately
|
continue; // Will launch the target activity immediately
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
currentActivity = std::move(stackActivities.back());
|
currentActivity = std::move(stackActivities.back());
|
||||||
@@ -149,6 +167,9 @@ void ActivityManager::loop() {
|
|||||||
RenderLock lock;
|
RenderLock lock;
|
||||||
|
|
||||||
if (pendingAction == PendingAction::Replace) {
|
if (pendingAction == PendingAction::Replace) {
|
||||||
|
#if DEBUG_MEMORY_CONSUMPTION
|
||||||
|
logActivityStackState("replace_before", currentActivity.get(), stackActivities.size());
|
||||||
|
#endif
|
||||||
// Destroy the current activity
|
// Destroy the current activity
|
||||||
exitActivity(lock);
|
exitActivity(lock);
|
||||||
// Clear the stack
|
// Clear the stack
|
||||||
@@ -156,10 +177,21 @@ void ActivityManager::loop() {
|
|||||||
stackActivities.back()->onExit();
|
stackActivities.back()->onExit();
|
||||||
stackActivities.pop_back();
|
stackActivities.pop_back();
|
||||||
}
|
}
|
||||||
|
#if DEBUG_MEMORY_CONSUMPTION
|
||||||
|
logActivityStackState("replace_after_clear", nullptr, stackActivities.size());
|
||||||
|
#endif
|
||||||
} else if (pendingAction == PendingAction::Push) {
|
} else if (pendingAction == PendingAction::Push) {
|
||||||
|
#if DEBUG_MEMORY_CONSUMPTION
|
||||||
|
logActivityStackState("push_before", currentActivity.get(), stackActivities.size());
|
||||||
|
#endif
|
||||||
// Move current activity to stack
|
// Move current activity to stack
|
||||||
stackActivities.push_back(std::move(currentActivity));
|
stackActivities.push_back(std::move(currentActivity));
|
||||||
|
#if DEBUG_MEMORY_CONSUMPTION
|
||||||
LOG_DBG("ACT", "Pushed to activity stack, new size = %zu", stackActivities.size());
|
LOG_DBG("ACT", "Pushed to activity stack, new size = %zu", stackActivities.size());
|
||||||
|
logActivityStackState("push_after", currentActivity.get(), stackActivities.size());
|
||||||
|
#else
|
||||||
|
LOG_DBG("ACT", "Pushed to activity stack, new size = %zu", stackActivities.size());
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
pendingAction = PendingAction::None;
|
pendingAction = PendingAction::None;
|
||||||
currentActivity = std::move(pendingActivity);
|
currentActivity = std::move(pendingActivity);
|
||||||
@@ -201,6 +233,10 @@ void ActivityManager::replaceActivity(std::unique_ptr<Activity>&& newActivity) {
|
|||||||
if (currentActivity) {
|
if (currentActivity) {
|
||||||
// Defer launch if we're currently in an activity, to avoid deleting the current activity
|
// Defer launch if we're currently in an activity, to avoid deleting the current activity
|
||||||
// leading to the "delete this" problem
|
// leading to the "delete this" problem
|
||||||
|
#if DEBUG_MEMORY_CONSUMPTION
|
||||||
|
LOG_DBG("ACT", "replaceActivity requested: current=%s stackSize=%zu", currentActivity->getName().c_str(),
|
||||||
|
stackActivities.size());
|
||||||
|
#endif
|
||||||
pendingActivity = std::move(newActivity);
|
pendingActivity = std::move(newActivity);
|
||||||
pendingAction = PendingAction::Replace;
|
pendingAction = PendingAction::Replace;
|
||||||
} else {
|
} else {
|
||||||
@@ -216,16 +252,19 @@ void ActivityManager::goToFileTransfer() {
|
|||||||
|
|
||||||
void ActivityManager::goToSettings() { replaceActivity(std::make_unique<SettingsActivity>(renderer, mappedInput)); }
|
void ActivityManager::goToSettings() { replaceActivity(std::make_unique<SettingsActivity>(renderer, mappedInput)); }
|
||||||
|
|
||||||
void ActivityManager::goToFileBrowser(std::string path) {
|
void ActivityManager::goToFileBrowser(std::string path, std::string focusName) {
|
||||||
replaceActivity(std::make_unique<FileBrowserActivity>(renderer, mappedInput, std::move(path)));
|
replaceActivity(std::make_unique<FileBrowserActivity>(renderer, mappedInput, std::move(path), std::move(focusName)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ActivityManager::goToRecentBooks() {
|
void ActivityManager::goToRecentBooks(int focusIndex) {
|
||||||
replaceActivity(std::make_unique<RecentBooksActivity>(renderer, mappedInput));
|
replaceActivity(std::make_unique<RecentBooksActivity>(renderer, mappedInput, focusIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ActivityManager::goToGlobalBookmarks() {
|
void ActivityManager::goToGlobalBookmarks() { goToGlobalBookmarks({}); }
|
||||||
replaceActivity(std::make_unique<GlobalBookmarksActivity>(renderer, mappedInput));
|
|
||||||
|
void ActivityManager::goToGlobalBookmarks(ReturnHint hint) {
|
||||||
|
hasReturnHint = false;
|
||||||
|
replaceActivity(std::make_unique<GlobalBookmarksActivity>(renderer, mappedInput, std::move(hint)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ActivityManager::goToBrowser() {
|
void ActivityManager::goToBrowser() {
|
||||||
@@ -249,8 +288,48 @@ void ActivityManager::goToKOReaderSync() {
|
|||||||
sync.hasParagraphIndex, sync.intent));
|
sync.hasParagraphIndex, sync.intent));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ActivityManager::pushReader(std::string path) {
|
void ActivityManager::replaceWithReader(std::string path, ReturnHint hint) {
|
||||||
pushActivity(std::make_unique<ReaderActivity>(renderer, mappedInput, std::move(path)));
|
returnHint = std::move(hint);
|
||||||
|
hasReturnHint = true;
|
||||||
|
replaceActivity(std::make_unique<ReaderActivity>(renderer, mappedInput, std::move(path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActivityManager::replaceWithFileBrowser(std::string path, ReturnHint hint, std::string focusName) {
|
||||||
|
returnHint = std::move(hint);
|
||||||
|
hasReturnHint = true;
|
||||||
|
replaceActivity(std::make_unique<FileBrowserActivity>(renderer, mappedInput, std::move(path), std::move(focusName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActivityManager::replaceWithRecentBooks(ReturnHint hint) {
|
||||||
|
returnHint = std::move(hint);
|
||||||
|
hasReturnHint = true;
|
||||||
|
replaceActivity(std::make_unique<RecentBooksActivity>(renderer, mappedInput, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ActivityManager::returnFromChild() {
|
||||||
|
if (!hasReturnHint) {
|
||||||
|
goHome();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ReturnHint hint = std::move(returnHint);
|
||||||
|
returnHint = {};
|
||||||
|
hasReturnHint = false;
|
||||||
|
|
||||||
|
switch (hint.target) {
|
||||||
|
case ReturnTo::FileBrowser:
|
||||||
|
goToFileBrowser(std::move(hint.path), std::move(hint.selectName));
|
||||||
|
break;
|
||||||
|
case ReturnTo::RecentBooks:
|
||||||
|
goToRecentBooks(hint.selectIndex);
|
||||||
|
break;
|
||||||
|
case ReturnTo::GlobalBookmarks:
|
||||||
|
goToGlobalBookmarks(std::move(hint));
|
||||||
|
break;
|
||||||
|
case ReturnTo::Home:
|
||||||
|
default:
|
||||||
|
goHome(std::move(hint.selectName), hint.selectIndex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ActivityManager::goToSleep() {
|
void ActivityManager::goToSleep() {
|
||||||
@@ -266,7 +345,10 @@ void ActivityManager::goToFullScreenMessage(std::string message, EpdFontFamily::
|
|||||||
|
|
||||||
void ActivityManager::goToWeather() { replaceActivity(std::make_unique<WeatherActivity>(renderer, mappedInput)); }
|
void ActivityManager::goToWeather() { replaceActivity(std::make_unique<WeatherActivity>(renderer, mappedInput)); }
|
||||||
|
|
||||||
void ActivityManager::goHome() { replaceActivity(std::make_unique<HomeActivity>(renderer, mappedInput)); }
|
void ActivityManager::goHome(std::string focusBookPath, int focusSelectorIndex) {
|
||||||
|
hasReturnHint = false;
|
||||||
|
replaceActivity(std::make_unique<HomeActivity>(renderer, mappedInput, std::move(focusBookPath), focusSelectorIndex));
|
||||||
|
}
|
||||||
|
|
||||||
void ActivityManager::pushActivity(std::unique_ptr<Activity>&& activity) {
|
void ActivityManager::pushActivity(std::unique_ptr<Activity>&& activity) {
|
||||||
if (pendingActivity) {
|
if (pendingActivity) {
|
||||||
@@ -274,6 +356,10 @@ void ActivityManager::pushActivity(std::unique_ptr<Activity>&& activity) {
|
|||||||
LOG_ERR("ACT", "pendingActivity while pushActivity is not expected");
|
LOG_ERR("ACT", "pendingActivity while pushActivity is not expected");
|
||||||
pendingActivity.reset();
|
pendingActivity.reset();
|
||||||
}
|
}
|
||||||
|
#if DEBUG_MEMORY_CONSUMPTION
|
||||||
|
LOG_DBG("ACT", "pushActivity requested: current=%s stackSize=%zu",
|
||||||
|
currentActivity ? currentActivity->getName().c_str() : "<none>", stackActivities.size());
|
||||||
|
#endif
|
||||||
pendingActivity = std::move(activity);
|
pendingActivity = std::move(activity);
|
||||||
pendingAction = PendingAction::Push;
|
pendingAction = PendingAction::Push;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,23 @@
|
|||||||
class Activity; // forward declaration
|
class Activity; // forward declaration
|
||||||
class RenderLock; // forward declaration
|
class RenderLock; // forward declaration
|
||||||
|
|
||||||
|
// Where a "child" activity (launched via one of the replaceWith* helpers) should route
|
||||||
|
// control when it exits successfully. See ActivityManager::returnFromChild().
|
||||||
|
enum class ReturnTo : uint8_t { Home, FileBrowser, RecentBooks, GlobalBookmarks };
|
||||||
|
|
||||||
|
// Minimal state the returning parent needs to restore its previous view (directory,
|
||||||
|
// focused item, list index, or bookmark selection). Kept as a plain struct stored by
|
||||||
|
// value on the ActivityManager — single instance, overwritten per transition, no heap
|
||||||
|
// churn beyond the small strings.
|
||||||
|
struct ReturnHint {
|
||||||
|
ReturnTo target = ReturnTo::Home;
|
||||||
|
std::string path; // FileBrowser directory to restore
|
||||||
|
std::string selectName; // item to re-focus in a list (file name, book title)
|
||||||
|
int selectIndex = -1; // e.g. Recents index
|
||||||
|
std::string selectionContext; // optional activity-specific restore key
|
||||||
|
int selectBookmarkIndex = -1; // optional bookmark index for GlobalBookmarks
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ActivityManager
|
* ActivityManager
|
||||||
*
|
*
|
||||||
@@ -68,6 +85,15 @@ class ActivityManager {
|
|||||||
// into the next one.
|
// into the next one.
|
||||||
bool drainInput = false;
|
bool drainInput = false;
|
||||||
|
|
||||||
|
// Where returnFromChild() should route to. Set by replaceWith*() helpers and
|
||||||
|
// preserved across plain goTo*() chains so a chained navigation flow can still
|
||||||
|
// restore its original parent state. Cleared only by returnFromChild() or by
|
||||||
|
// explicit goHome()/replaceWith*() calls, not by ordinary goTo*() transitions.
|
||||||
|
// Relevant symbols: ReturnHint, returnHint, hasReturnHint, returnFromChild(),
|
||||||
|
// goHome(), goTo*(), replaceWith*().
|
||||||
|
ReturnHint returnHint;
|
||||||
|
bool hasReturnHint = false;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ActivityManager(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
explicit ActivityManager(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||||
: renderer(renderer), mappedInput(mappedInput), renderingMutex(xSemaphoreCreateMutex()) {
|
: renderer(renderer), mappedInput(mappedInput), renderingMutex(xSemaphoreCreateMutex()) {
|
||||||
@@ -85,18 +111,43 @@ class ActivityManager {
|
|||||||
// goTo... functions are convenient wrapper for replaceActivity()
|
// goTo... functions are convenient wrapper for replaceActivity()
|
||||||
void goToFileTransfer();
|
void goToFileTransfer();
|
||||||
void goToSettings();
|
void goToSettings();
|
||||||
void goToFileBrowser(std::string path = {});
|
void goToFileBrowser(std::string path = {}, std::string focusName = {});
|
||||||
void goToRecentBooks();
|
void goToRecentBooks(int focusIndex = -1);
|
||||||
void goToGlobalBookmarks();
|
void goToGlobalBookmarks();
|
||||||
|
void goToGlobalBookmarks(ReturnHint hint);
|
||||||
void goToBrowser();
|
void goToBrowser();
|
||||||
void goToReader(std::string path);
|
void goToReader(std::string path);
|
||||||
void goToKOReaderSync();
|
void goToKOReaderSync();
|
||||||
void pushReader(std::string path);
|
|
||||||
void goToSleep();
|
void goToSleep();
|
||||||
void goToBoot();
|
void goToBoot();
|
||||||
void goToFullScreenMessage(std::string message, EpdFontFamily::Style style = EpdFontFamily::REGULAR);
|
void goToFullScreenMessage(std::string message, EpdFontFamily::Style style = EpdFontFamily::REGULAR);
|
||||||
void goToWeather();
|
void goToWeather();
|
||||||
void goHome();
|
void goHome(std::string focusBookPath = {}, int focusSelectorIndex = -1);
|
||||||
|
|
||||||
|
// Replace-with-hint helpers: destroy the current activity before launching the new
|
||||||
|
// one (freeing its memory) and record where to route control when the new activity
|
||||||
|
// exits. Consumed by returnFromChild().
|
||||||
|
void replaceWithReader(std::string path, ReturnHint hint);
|
||||||
|
void replaceWithFileBrowser(std::string path, ReturnHint hint, std::string focusName = {});
|
||||||
|
void replaceWithRecentBooks(ReturnHint hint);
|
||||||
|
|
||||||
|
// Called by a "child" activity on successful exit. Consults the stored ReturnHint,
|
||||||
|
// clears it, and dispatches to the corresponding parent with restoration args. If
|
||||||
|
// no hint is set, defaults to goHome().
|
||||||
|
void returnFromChild();
|
||||||
|
|
||||||
|
// Record a ReturnHint before calling any plain goTo*() helper. Allows an activity
|
||||||
|
// (e.g. Home) to declare "when this flow ends, come back here with this state" for
|
||||||
|
// transitions where we don't want a dedicated replaceWith*() wrapper.
|
||||||
|
// Cleared by returnFromChild() or by an explicit goHome()/replaceWith*() call.
|
||||||
|
void setReturnHint(ReturnHint hint) {
|
||||||
|
returnHint = std::move(hint);
|
||||||
|
hasReturnHint = true;
|
||||||
|
}
|
||||||
|
void clearReturnHint() {
|
||||||
|
returnHint = {};
|
||||||
|
hasReturnHint = false;
|
||||||
|
}
|
||||||
|
|
||||||
// This will move current activity to stack instead of deleting it
|
// This will move current activity to stack instead of deleting it
|
||||||
void pushActivity(std::unique_ptr<Activity>&& activity);
|
void pushActivity(std::unique_ptr<Activity>&& activity);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "../ActivityManager.h"
|
||||||
#include "../util/ConfirmationActivity.h"
|
#include "../util/ConfirmationActivity.h"
|
||||||
#include "BookInfoActivity.h"
|
#include "BookInfoActivity.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
@@ -113,6 +114,14 @@ void FileBrowserActivity::onEnter() {
|
|||||||
loadFiles();
|
loadFiles();
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
|
|
||||||
|
if (!focusName.empty()) {
|
||||||
|
const size_t idx = findEntry(focusName);
|
||||||
|
if (idx < files.size()) {
|
||||||
|
selectorIndex = idx;
|
||||||
|
}
|
||||||
|
focusName.clear();
|
||||||
|
}
|
||||||
|
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +156,8 @@ void FileBrowserActivity::loop() {
|
|||||||
loadFiles();
|
loadFiles();
|
||||||
const auto pos = oldPath.find_last_of('/');
|
const auto pos = oldPath.find_last_of('/');
|
||||||
const std::string dirName = oldPath.substr(pos + 1) + "/";
|
const std::string dirName = oldPath.substr(pos + 1) + "/";
|
||||||
selectorIndex = findEntry(dirName);
|
const size_t idx = findEntry(dirName);
|
||||||
|
selectorIndex = (idx < files.size()) ? idx : 0;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
} else {
|
} else {
|
||||||
onGoHome();
|
onGoHome();
|
||||||
@@ -171,7 +181,12 @@ void FileBrowserActivity::loop() {
|
|||||||
} else {
|
} else {
|
||||||
std::string fullPath = basepath;
|
std::string fullPath = basepath;
|
||||||
if (fullPath.back() != '/') fullPath += "/";
|
if (fullPath.back() != '/') fullPath += "/";
|
||||||
onSelectBook(fullPath + entry);
|
fullPath += entry;
|
||||||
|
ReturnHint hint;
|
||||||
|
hint.target = ReturnTo::FileBrowser;
|
||||||
|
hint.path = basepath;
|
||||||
|
hint.selectName = entry;
|
||||||
|
activityManager.replaceWithReader(std::move(fullPath), std::move(hint));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -309,5 +324,5 @@ void FileBrowserActivity::render(RenderLock&&) {
|
|||||||
size_t FileBrowserActivity::findEntry(const std::string& name) const {
|
size_t FileBrowserActivity::findEntry(const std::string& name) const {
|
||||||
for (size_t i = 0; i < files.size(); i++)
|
for (size_t i = 0; i < files.size(); i++)
|
||||||
if (files[i] == name) return i;
|
if (files[i] == name) return i;
|
||||||
return 0;
|
return files.size();
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,7 @@ class FileBrowserActivity final : public Activity {
|
|||||||
|
|
||||||
// Files state
|
// Files state
|
||||||
std::string basepath = "/";
|
std::string basepath = "/";
|
||||||
|
std::string focusName; // entry to select on first load (e.g. the file just returned from)
|
||||||
std::vector<std::string> files;
|
std::vector<std::string> files;
|
||||||
|
|
||||||
// Data loading
|
// Data loading
|
||||||
@@ -26,8 +27,11 @@ class FileBrowserActivity final : public Activity {
|
|||||||
size_t findEntry(const std::string& name) const;
|
size_t findEntry(const std::string& name) const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit FileBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialPath = "/")
|
explicit FileBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialPath = "/",
|
||||||
: Activity("FileBrowser", renderer, mappedInput), basepath(initialPath.empty() ? "/" : std::move(initialPath)) {}
|
std::string focusName = {})
|
||||||
|
: Activity("FileBrowser", renderer, mappedInput),
|
||||||
|
basepath(initialPath.empty() ? "/" : std::move(initialPath)),
|
||||||
|
focusName(std::move(focusName)) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
|
||||||
|
#include "../ActivityManager.h"
|
||||||
#include "BookmarkStore.h"
|
#include "BookmarkStore.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
#include "GlobalBookmarkIndex.h"
|
#include "GlobalBookmarkIndex.h"
|
||||||
@@ -26,6 +27,26 @@ void GlobalBookmarksActivity::onEnter() {
|
|||||||
const int first = firstSelectableIndex();
|
const int first = firstSelectableIndex();
|
||||||
selectorIndex = first >= 0 ? first : 0;
|
selectorIndex = first >= 0 ? first : 0;
|
||||||
|
|
||||||
|
if (restoreHint.target == ReturnTo::GlobalBookmarks) {
|
||||||
|
const auto& entries = GLOBAL_BOOKMARKS.getEntries();
|
||||||
|
if (!restoreHint.selectionContext.empty() && restoreHint.selectBookmarkIndex >= 0) {
|
||||||
|
for (size_t i = 0; i < rows.size(); ++i) {
|
||||||
|
const auto& row = rows[i];
|
||||||
|
if (row.isSeparator) continue;
|
||||||
|
if (row.bookmarkIndex == static_cast<size_t>(restoreHint.selectBookmarkIndex) &&
|
||||||
|
row.bookIndex < entries.size() && entries[row.bookIndex].sourcePath == restoreHint.selectionContext) {
|
||||||
|
selectorIndex = static_cast<int>(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selectorIndex < 0 || selectorIndex >= static_cast<int>(rows.size()) || isSeparatorRow(selectorIndex)) {
|
||||||
|
const int fallback = firstSelectableIndex();
|
||||||
|
selectorIndex = fallback >= 0 ? fallback : 0;
|
||||||
|
}
|
||||||
|
restoreHint = {};
|
||||||
|
}
|
||||||
|
|
||||||
const auto total = static_cast<int>(rows.size());
|
const auto total = static_cast<int>(rows.size());
|
||||||
buttonNavigator.setSelectablePredicate([this](int index) { return !isSeparatorRow(index); }, total);
|
buttonNavigator.setSelectablePredicate([this](int index) { return !isSeparatorRow(index); }, total);
|
||||||
|
|
||||||
@@ -124,7 +145,11 @@ void GlobalBookmarksActivity::openSelected() {
|
|||||||
APP_STATE.saveToFile();
|
APP_STATE.saveToFile();
|
||||||
|
|
||||||
LOG_DBG("GBA", "Jumping to bookmark in %s at %u/%u", entry.sourcePath.c_str(), bm.spineIndex, bm.pageNumber);
|
LOG_DBG("GBA", "Jumping to bookmark in %s at %u/%u", entry.sourcePath.c_str(), bm.spineIndex, bm.pageNumber);
|
||||||
onSelectBook(entry.sourcePath);
|
ReturnHint hint;
|
||||||
|
hint.target = ReturnTo::GlobalBookmarks;
|
||||||
|
hint.selectionContext = entry.sourcePath;
|
||||||
|
hint.selectBookmarkIndex = static_cast<int>(row.bookmarkIndex);
|
||||||
|
activityManager.replaceWithReader(entry.sourcePath, std::move(hint));
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename Op>
|
template <typename Op>
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ struct Rect;
|
|||||||
// header separator or a bookmark entry belonging to the preceding header.
|
// header separator or a bookmark entry belonging to the preceding header.
|
||||||
class GlobalBookmarksActivity final : public Activity {
|
class GlobalBookmarksActivity final : public Activity {
|
||||||
public:
|
public:
|
||||||
explicit GlobalBookmarksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
explicit GlobalBookmarksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, ReturnHint restoreHint = {})
|
||||||
: Activity("GlobalBookmarks", renderer, mappedInput) {}
|
: Activity("GlobalBookmarks", renderer, mappedInput), restoreHint(std::move(restoreHint)) {}
|
||||||
|
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
@@ -38,6 +38,7 @@ class GlobalBookmarksActivity final : public Activity {
|
|||||||
ButtonNavigator buttonNavigator;
|
ButtonNavigator buttonNavigator;
|
||||||
std::vector<Row> rows;
|
std::vector<Row> rows;
|
||||||
int selectorIndex = 0;
|
int selectorIndex = 0;
|
||||||
|
ReturnHint restoreHint;
|
||||||
|
|
||||||
void rebuildRows();
|
void rebuildRows();
|
||||||
std::string getRowTitle(int index) const;
|
std::string getRowTitle(int index) const;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <Utf8.h>
|
#include <Utf8.h>
|
||||||
#include <Xtc.h>
|
#include <Xtc.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -211,6 +212,28 @@ void HomeActivity::onEnter() {
|
|||||||
recentsLoaded = true;
|
recentsLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply focus: book path takes priority, else combined selector index (covers
|
||||||
|
// "return to the menu entry I was on").
|
||||||
|
bool focused = false;
|
||||||
|
if (!focusBookPath.empty()) {
|
||||||
|
for (size_t i = 0; i < recentBooks.size(); ++i) {
|
||||||
|
if (recentBooks[i].path == focusBookPath) {
|
||||||
|
selectorIndex = static_cast<int>(i);
|
||||||
|
focused = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
focusBookPath.clear();
|
||||||
|
}
|
||||||
|
if (!focused && focusSelectorIndex >= 0) {
|
||||||
|
rebuildMenuEntries(); // need menu count to clamp; rebuild is idempotent
|
||||||
|
const int combinedSize = static_cast<int>(recentBooks.size() + menuEntries.size());
|
||||||
|
if (combinedSize > 0) {
|
||||||
|
selectorIndex = std::min(focusSelectorIndex, combinedSize - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
focusSelectorIndex = -1;
|
||||||
|
|
||||||
// Trigger first update
|
// Trigger first update
|
||||||
menuEntriesDirty = true;
|
menuEntriesDirty = true;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
@@ -348,9 +371,21 @@ void HomeActivity::render(RenderLock&&) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void HomeActivity::onSelectBook(const std::string& path) { activityManager.pushReader(path); }
|
void HomeActivity::onSelectBook(const std::string& path) {
|
||||||
|
ReturnHint hint;
|
||||||
|
hint.target = ReturnTo::Home;
|
||||||
|
hint.selectName = path; // used to re-focus the book in the recents strip after return
|
||||||
|
activityManager.replaceWithReader(path, std::move(hint));
|
||||||
|
}
|
||||||
|
|
||||||
void HomeActivity::dispatchMenuAction(MenuAction action) {
|
void HomeActivity::dispatchMenuAction(MenuAction action) {
|
||||||
|
// Record where the menu entry was focused so that when the launched activity exits
|
||||||
|
// (via returnFromChild() or an empty-stack finish()), we come back to the same row.
|
||||||
|
ReturnHint hint;
|
||||||
|
hint.target = ReturnTo::Home;
|
||||||
|
hint.selectIndex = selectorIndex;
|
||||||
|
activityManager.setReturnHint(std::move(hint));
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case MenuAction::FileBrowser:
|
case MenuAction::FileBrowser:
|
||||||
activityManager.goToFileBrowser();
|
activityManager.goToFileBrowser();
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ class HomeActivity final : public Activity {
|
|||||||
std::vector<MenuEntry> menuEntries;
|
std::vector<MenuEntry> menuEntries;
|
||||||
bool menuEntriesDirty = true;
|
bool menuEntriesDirty = true;
|
||||||
|
|
||||||
|
std::string focusBookPath; // book path to re-select on first render, if present in recents
|
||||||
|
int focusSelectorIndex = -1; // fallback combined-selector index when focusBookPath doesn't match
|
||||||
|
|
||||||
void onSelectBook(const std::string& path);
|
void onSelectBook(const std::string& path);
|
||||||
void dispatchMenuAction(MenuAction action);
|
void dispatchMenuAction(MenuAction action);
|
||||||
|
|
||||||
@@ -55,8 +58,11 @@ class HomeActivity final : public Activity {
|
|||||||
void loadRecentCovers(int coverHeight);
|
void loadRecentCovers(int coverHeight);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string focusBookPath = {},
|
||||||
: Activity("Home", renderer, mappedInput) {}
|
int focusSelectorIndex = -1)
|
||||||
|
: Activity("Home", renderer, mappedInput),
|
||||||
|
focusBookPath(std::move(focusBookPath)),
|
||||||
|
focusSelectorIndex(focusSelectorIndex) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "../ActivityManager.h"
|
||||||
#include "../util/ConfirmationActivity.h"
|
#include "../util/ConfirmationActivity.h"
|
||||||
#include "BookInfoActivity.h"
|
#include "BookInfoActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
@@ -35,6 +36,10 @@ void RecentBooksActivity::onEnter() {
|
|||||||
loadRecentBooks();
|
loadRecentBooks();
|
||||||
|
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
|
if (initialFocusIndex >= 0 && static_cast<size_t>(initialFocusIndex) < recentBooks.size()) {
|
||||||
|
selectorIndex = static_cast<size_t>(initialFocusIndex);
|
||||||
|
}
|
||||||
|
initialFocusIndex = -1;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +54,10 @@ void RecentBooksActivity::loop() {
|
|||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) && !recentBooks.empty() &&
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) && !recentBooks.empty() &&
|
||||||
selectorIndex < static_cast<int>(recentBooks.size())) {
|
selectorIndex < static_cast<int>(recentBooks.size())) {
|
||||||
LOG_DBG("RBA", "Selected recent book: %s", recentBooks[selectorIndex].path.c_str());
|
LOG_DBG("RBA", "Selected recent book: %s", recentBooks[selectorIndex].path.c_str());
|
||||||
onSelectBook(recentBooks[selectorIndex].path);
|
ReturnHint hint;
|
||||||
|
hint.target = ReturnTo::RecentBooks;
|
||||||
|
hint.selectIndex = static_cast<int>(selectorIndex);
|
||||||
|
activityManager.replaceWithReader(recentBooks[selectorIndex].path, std::move(hint));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class RecentBooksActivity final : public Activity {
|
|||||||
ButtonNavigator buttonNavigator;
|
ButtonNavigator buttonNavigator;
|
||||||
|
|
||||||
size_t selectorIndex = 0;
|
size_t selectorIndex = 0;
|
||||||
|
int initialFocusIndex = -1; // applied once in onEnter(), then cleared
|
||||||
|
|
||||||
// Recent tab state
|
// Recent tab state
|
||||||
std::vector<RecentBook> recentBooks;
|
std::vector<RecentBook> recentBooks;
|
||||||
@@ -22,8 +23,8 @@ class RecentBooksActivity final : public Activity {
|
|||||||
void loadRecentBooks();
|
void loadRecentBooks();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit RecentBooksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
explicit RecentBooksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, int focusIndex = -1)
|
||||||
: Activity("RecentBooks", renderer, mappedInput) {}
|
: Activity("RecentBooks", renderer, mappedInput), initialFocusIndex(focusIndex) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
#ifndef DEBUG_MEMORY_CONSUMPTION
|
||||||
|
#define DEBUG_MEMORY_CONSUMPTION 0
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "EpubReaderActivity.h"
|
#include "EpubReaderActivity.h"
|
||||||
|
|
||||||
#include <Epub/Page.h>
|
#include <Epub/Page.h>
|
||||||
@@ -35,11 +39,15 @@ constexpr unsigned long skipChapterMs = 700;
|
|||||||
// pages per minute, first item is 1 to prevent division by zero if accessed
|
// pages per minute, first item is 1 to prevent division by zero if accessed
|
||||||
constexpr int PAGE_TURN_LABELS[] = {1, 1, 3, 6, 12};
|
constexpr int PAGE_TURN_LABELS[] = {1, 1, 3, 6, 12};
|
||||||
|
|
||||||
|
#if DEBUG_MEMORY_CONSUMPTION
|
||||||
void logReaderMemSnapshot(const char* stage) {
|
void logReaderMemSnapshot(const char* stage) {
|
||||||
const uint32_t freeHeap = esp_get_free_heap_size();
|
const uint32_t freeHeap = esp_get_free_heap_size();
|
||||||
const uint32_t contigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
const uint32_t contigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||||
LOG_DBG("ERS", "Reader mem[%s]: free=%lu contig=%lu", stage, freeHeap, contigHeap);
|
LOG_DBG("ERS", "Reader mem[%s]: free=%lu contig=%lu", stage, freeHeap, contigHeap);
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
inline void logReaderMemSnapshot(const char*) {}
|
||||||
|
#endif
|
||||||
|
|
||||||
bool writeReaderProgressCache(const std::string& cachePath, const int spineIndex, const int currentPage,
|
bool writeReaderProgressCache(const std::string& cachePath, const int spineIndex, const int currentPage,
|
||||||
const int pageCount) {
|
const int pageCount) {
|
||||||
@@ -91,10 +99,13 @@ void EpubReaderActivity::onEnter() {
|
|||||||
RenderLock lock(*this);
|
RenderLock lock(*this);
|
||||||
ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);
|
ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);
|
||||||
}
|
}
|
||||||
|
logReaderMemSnapshot("onEnter_after_orientation");
|
||||||
|
|
||||||
epub->setupCacheDir();
|
epub->setupCacheDir();
|
||||||
|
logReaderMemSnapshot("onEnter_after_setupCacheDir");
|
||||||
applyPendingSyncSession();
|
applyPendingSyncSession();
|
||||||
applyPendingBookmarkJump();
|
applyPendingBookmarkJump();
|
||||||
|
logReaderMemSnapshot("onEnter_after_pending_sync");
|
||||||
|
|
||||||
FsFile f;
|
FsFile f;
|
||||||
if (Storage.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) {
|
if (Storage.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) {
|
||||||
@@ -120,9 +131,11 @@ void EpubReaderActivity::onEnter() {
|
|||||||
LOG_DBG("ERS", "Opened for first time, navigating to text reference at index %d", textSpineIndex);
|
LOG_DBG("ERS", "Opened for first time, navigating to text reference at index %d", textSpineIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
logReaderMemSnapshot("onEnter_after_progress_load");
|
||||||
|
|
||||||
// Load bookmarks for this book
|
// Load bookmarks for this book
|
||||||
bookmarkStore.load(epub->getCachePath());
|
bookmarkStore.load(epub->getCachePath());
|
||||||
|
logReaderMemSnapshot("onEnter_after_bookmarks_loaded");
|
||||||
|
|
||||||
// Save current epub as last opened epub and add to recent books
|
// Save current epub as last opened epub and add to recent books
|
||||||
APP_STATE.openEpubPath = epub->getPath();
|
APP_STATE.openEpubPath = epub->getPath();
|
||||||
@@ -135,8 +148,10 @@ void EpubReaderActivity::onEnter() {
|
|||||||
const RecentBook currentBook = RECENT_BOOKS.getBookByPath(epub->getPath());
|
const RecentBook currentBook = RECENT_BOOKS.getBookByPath(epub->getPath());
|
||||||
bookEmbeddedStyleOverride = currentBook.embeddedStyleOverride;
|
bookEmbeddedStyleOverride = currentBook.embeddedStyleOverride;
|
||||||
bookImageRenderingOverride = currentBook.imageRenderingOverride;
|
bookImageRenderingOverride = currentBook.imageRenderingOverride;
|
||||||
|
logReaderMemSnapshot("onEnter_after_recent_books");
|
||||||
|
|
||||||
// Trigger first update
|
// Trigger first update
|
||||||
|
logReaderMemSnapshot("onEnter_before_request_update");
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
logReaderMemSnapshot("onEnter_ready");
|
logReaderMemSnapshot("onEnter_ready");
|
||||||
}
|
}
|
||||||
@@ -1092,8 +1107,10 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
const int orientedMarginRight, const int orientedMarginBottom,
|
const int orientedMarginRight, const int orientedMarginBottom,
|
||||||
const int orientedMarginLeft) {
|
const int orientedMarginLeft) {
|
||||||
const auto t0 = millis();
|
const auto t0 = millis();
|
||||||
|
logReaderMemSnapshot("render_start");
|
||||||
auto* fcm = renderer.getFontCacheManager();
|
auto* fcm = renderer.getFontCacheManager();
|
||||||
fcm->resetStats();
|
fcm->resetStats();
|
||||||
|
logReaderMemSnapshot("prewarm_begin");
|
||||||
|
|
||||||
// Font prewarm: scan pass accumulates text, then prewarm, then real render
|
// Font prewarm: scan pass accumulates text, then prewarm, then real render
|
||||||
const uint32_t heapBefore = esp_get_free_heap_size();
|
const uint32_t heapBefore = esp_get_free_heap_size();
|
||||||
@@ -1106,14 +1123,17 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
|
|
||||||
LOG_DBG("ERS", "Heap: before=%lu after=%lu delta=%ld", heapBefore, heapAfter,
|
LOG_DBG("ERS", "Heap: before=%lu after=%lu delta=%ld", heapBefore, heapAfter,
|
||||||
(int32_t)heapAfter - (int32_t)heapBefore);
|
(int32_t)heapAfter - (int32_t)heapBefore);
|
||||||
|
logReaderMemSnapshot("prewarm_end");
|
||||||
|
|
||||||
// Force special handling for pages with images when anti-aliasing is on
|
// Force special handling for pages with images when anti-aliasing is on
|
||||||
bool imagePageWithAA = page->hasImages() && SETTINGS.textAntiAliasing;
|
bool imagePageWithAA = page->hasImages() && SETTINGS.textAntiAliasing;
|
||||||
|
|
||||||
|
logReaderMemSnapshot("before_bw_render");
|
||||||
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
||||||
renderStatusBar();
|
renderStatusBar();
|
||||||
fcm->logStats("bw_render");
|
fcm->logStats("bw_render");
|
||||||
const auto tBwRender = millis();
|
const auto tBwRender = millis();
|
||||||
|
logReaderMemSnapshot("after_bw_render");
|
||||||
|
|
||||||
if (imagePageWithAA) {
|
if (imagePageWithAA) {
|
||||||
// Double FAST_REFRESH with selective image blanking (pablohc's technique):
|
// Double FAST_REFRESH with selective image blanking (pablohc's technique):
|
||||||
@@ -1140,34 +1160,44 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
const auto tDisplay = millis();
|
const auto tDisplay = millis();
|
||||||
|
|
||||||
// Save bw buffer to reset buffer state after grayscale data sync
|
// Save bw buffer to reset buffer state after grayscale data sync
|
||||||
|
logReaderMemSnapshot("bw_store_begin");
|
||||||
renderer.storeBwBuffer();
|
renderer.storeBwBuffer();
|
||||||
const auto tBwStore = millis();
|
const auto tBwStore = millis();
|
||||||
|
logReaderMemSnapshot("bw_store_end");
|
||||||
|
|
||||||
// grayscale rendering
|
// grayscale rendering
|
||||||
// TODO: Only do this if font supports it
|
// TODO: Only do this if font supports it
|
||||||
if (SETTINGS.textAntiAliasing) {
|
if (SETTINGS.textAntiAliasing) {
|
||||||
|
logReaderMemSnapshot("gray_lsb_begin");
|
||||||
renderer.clearScreen(0x00);
|
renderer.clearScreen(0x00);
|
||||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||||
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
||||||
renderer.copyGrayscaleLsbBuffers();
|
renderer.copyGrayscaleLsbBuffers();
|
||||||
const auto tGrayLsb = millis();
|
const auto tGrayLsb = millis();
|
||||||
|
logReaderMemSnapshot("gray_lsb_end");
|
||||||
|
|
||||||
// Render and copy to MSB buffer
|
// Render and copy to MSB buffer
|
||||||
|
logReaderMemSnapshot("gray_msb_begin");
|
||||||
renderer.clearScreen(0x00);
|
renderer.clearScreen(0x00);
|
||||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
|
||||||
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
|
||||||
renderer.copyGrayscaleMsbBuffers();
|
renderer.copyGrayscaleMsbBuffers();
|
||||||
const auto tGrayMsb = millis();
|
const auto tGrayMsb = millis();
|
||||||
|
logReaderMemSnapshot("gray_msb_end");
|
||||||
|
|
||||||
// display grayscale part
|
// display grayscale part
|
||||||
|
logReaderMemSnapshot("gray_display_begin");
|
||||||
renderer.displayGrayBuffer();
|
renderer.displayGrayBuffer();
|
||||||
const auto tGrayDisplay = millis();
|
const auto tGrayDisplay = millis();
|
||||||
renderer.setRenderMode(GfxRenderer::BW);
|
renderer.setRenderMode(GfxRenderer::BW);
|
||||||
fcm->logStats("gray");
|
fcm->logStats("gray");
|
||||||
|
logReaderMemSnapshot("gray_display_end");
|
||||||
|
|
||||||
// restore the bw data
|
// restore the bw data
|
||||||
|
logReaderMemSnapshot("bw_restore_begin");
|
||||||
renderer.restoreBwBuffer();
|
renderer.restoreBwBuffer();
|
||||||
const auto tBwRestore = millis();
|
const auto tBwRestore = millis();
|
||||||
|
logReaderMemSnapshot("bw_restore_end");
|
||||||
|
|
||||||
const auto tEnd = millis();
|
const auto tEnd = millis();
|
||||||
LOG_DBG("ERS",
|
LOG_DBG("ERS",
|
||||||
@@ -1177,8 +1207,10 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
tGrayMsb - tGrayLsb, tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0);
|
tGrayMsb - tGrayLsb, tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0);
|
||||||
} else {
|
} else {
|
||||||
// restore the bw data
|
// restore the bw data
|
||||||
|
logReaderMemSnapshot("bw_restore_begin");
|
||||||
renderer.restoreBwBuffer();
|
renderer.restoreBwBuffer();
|
||||||
const auto tBwRestore = millis();
|
const auto tBwRestore = millis();
|
||||||
|
logReaderMemSnapshot("bw_restore_end");
|
||||||
|
|
||||||
const auto tEnd = millis();
|
const auto tEnd = millis();
|
||||||
LOG_DBG("ERS",
|
LOG_DBG("ERS",
|
||||||
|
|||||||
@@ -20,12 +20,20 @@
|
|||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
#ifndef DEBUG_MEMORY_CONSUMPTION
|
||||||
|
#define DEBUG_MEMORY_CONSUMPTION 0
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
#if DEBUG_MEMORY_CONSUMPTION
|
||||||
void logReaderLaunchMemSnapshot(const char* stage) {
|
void logReaderLaunchMemSnapshot(const char* stage) {
|
||||||
const uint32_t freeHeap = esp_get_free_heap_size();
|
const uint32_t freeHeap = esp_get_free_heap_size();
|
||||||
const uint32_t contigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
const uint32_t contigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||||
LOG_DBG("READER", "Reader mem[%s]: free=%lu contig=%lu", stage, freeHeap, contigHeap);
|
LOG_DBG("READER", "Reader mem[%s]: free=%lu contig=%lu", stage, freeHeap, contigHeap);
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
inline void logReaderLaunchMemSnapshot(const char*) {}
|
||||||
|
#endif
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::string ReaderActivity::extractFolderPath(const std::string& filePath) {
|
std::string ReaderActivity::extractFolderPath(const std::string& filePath) {
|
||||||
@@ -102,28 +110,24 @@ void ReaderActivity::goToLibrary(const std::string& fromBookPath) {
|
|||||||
void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
|
void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
|
||||||
const auto epubPath = epub->getPath();
|
const auto epubPath = epub->getPath();
|
||||||
currentBookPath = epubPath;
|
currentBookPath = epubPath;
|
||||||
logReaderLaunchMemSnapshot("before_push_epub_reader");
|
logReaderLaunchMemSnapshot("before_replace_epub_reader");
|
||||||
startActivityForResult(std::make_unique<EpubReaderActivity>(renderer, mappedInput, std::move(epub)),
|
activityManager.replaceActivity(std::make_unique<EpubReaderActivity>(renderer, mappedInput, std::move(epub)));
|
||||||
[this](const ActivityResult&) { finish(); });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReaderActivity::onGoToBmpViewer(const std::string& path) {
|
void ReaderActivity::onGoToBmpViewer(const std::string& path) {
|
||||||
startActivityForResult(std::make_unique<BmpViewerActivity>(renderer, mappedInput, path),
|
activityManager.replaceActivity(std::make_unique<BmpViewerActivity>(renderer, mappedInput, path));
|
||||||
[this](const ActivityResult&) { finish(); });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReaderActivity::onGoToXtcReader(std::unique_ptr<Xtc> xtc) {
|
void ReaderActivity::onGoToXtcReader(std::unique_ptr<Xtc> xtc) {
|
||||||
const auto xtcPath = xtc->getPath();
|
const auto xtcPath = xtc->getPath();
|
||||||
currentBookPath = xtcPath;
|
currentBookPath = xtcPath;
|
||||||
startActivityForResult(std::make_unique<XtcReaderActivity>(renderer, mappedInput, std::move(xtc)),
|
activityManager.replaceActivity(std::make_unique<XtcReaderActivity>(renderer, mappedInput, std::move(xtc)));
|
||||||
[this](const ActivityResult&) { finish(); });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReaderActivity::onGoToTxtReader(std::unique_ptr<Txt> txt) {
|
void ReaderActivity::onGoToTxtReader(std::unique_ptr<Txt> txt) {
|
||||||
const auto txtPath = txt->getPath();
|
const auto txtPath = txt->getPath();
|
||||||
currentBookPath = txtPath;
|
currentBookPath = txtPath;
|
||||||
startActivityForResult(std::make_unique<TxtReaderActivity>(renderer, mappedInput, std::move(txt)),
|
activityManager.replaceActivity(std::make_unique<TxtReaderActivity>(renderer, mappedInput, std::move(txt)));
|
||||||
[this](const ActivityResult&) { finish(); });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReaderActivity::onEnter() {
|
void ReaderActivity::onEnter() {
|
||||||
|
|||||||
@@ -24,13 +24,14 @@ void KOReaderSettingsActivity::buildMenuItems() {
|
|||||||
menuItems.push_back(SettingInfo::Action(StrId::STR_PASSWORD, SettingAction::None));
|
menuItems.push_back(SettingInfo::Action(StrId::STR_PASSWORD, SettingAction::None));
|
||||||
|
|
||||||
// Document matching: DynamicEnum toggling between Filename and Binary
|
// Document matching: DynamicEnum toggling between Filename and Binary
|
||||||
menuItems.push_back(SettingInfo::DynamicEnum(
|
menuItems.push_back(SettingInfo::DynamicEnum(StrId::STR_DOCUMENT_MATCHING, {StrId::STR_FILENAME, StrId::STR_BINARY},
|
||||||
StrId::STR_DOCUMENT_MATCHING, {StrId::STR_FILENAME, StrId::STR_BINARY},
|
static_cast<SettingInfo::ValueGetterFn>([](const void*) -> uint8_t {
|
||||||
static_cast<SettingInfo::ValueGetterFn>([](const void*) -> uint8_t { return static_cast<uint8_t>(KOREADER_STORE.getMatchMethod()); }),
|
return static_cast<uint8_t>(KOREADER_STORE.getMatchMethod());
|
||||||
[](void*, uint8_t v) {
|
}),
|
||||||
KOREADER_STORE.setMatchMethod(static_cast<DocumentMatchMethod>(v));
|
[](void*, uint8_t v) {
|
||||||
KOREADER_STORE.saveToFile();
|
KOREADER_STORE.setMatchMethod(static_cast<DocumentMatchMethod>(v));
|
||||||
}));
|
KOREADER_STORE.saveToFile();
|
||||||
|
}));
|
||||||
|
|
||||||
// Authenticate and Register: ACTION items
|
// Authenticate and Register: ACTION items
|
||||||
menuItems.push_back(
|
menuItems.push_back(
|
||||||
|
|||||||
Reference in New Issue
Block a user