diff --git a/src/activities/Activity.cpp b/src/activities/Activity.cpp index c725c2d5..f95b4dd1 100644 --- a/src/activities/Activity.cpp +++ b/src/activities/Activity.cpp @@ -1,61 +1,28 @@ #include "Activity.h" -#include +#include "ActivityManager.h" -void Activity::renderTaskTrampoline(void* param) { - auto* self = static_cast(param); - self->renderTaskLoop(); -} +void Activity::onEnter() { LOG_DBG("ACT", "Entering activity: %s", name.c_str()); } -void Activity::renderTaskLoop() { - while (true) { - ulTaskNotifyTake(pdTRUE, portMAX_DELAY); - { - HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering - RenderLock lock(*this); - render(std::move(lock)); - } - } -} +void Activity::onExit() { LOG_DBG("ACT", "Exiting activity: %s", name.c_str()); } -void Activity::onEnter() { - xTaskCreate(&renderTaskTrampoline, name.c_str(), - 8192, // Stack size - this, // Parameters - 1, // Priority - &renderTaskHandle // Task handle - ); - assert(renderTaskHandle != nullptr && "Failed to create render task"); - LOG_DBG("ACT", "Entering activity: %s", name.c_str()); -} - -void Activity::onExit() { - RenderLock lock(*this); // Ensure we don't delete the task while it's rendering - if (renderTaskHandle) { - vTaskDelete(renderTaskHandle); - renderTaskHandle = nullptr; - } - - LOG_DBG("ACT", "Exiting activity: %s", name.c_str()); -} - -void Activity::requestUpdate() { - // Using direct notification to signal the render task to update - // Increment counter so multiple rapid calls won't be lost - if (renderTaskHandle) { - xTaskNotify(renderTaskHandle, 1, eIncrement); - } -} +void Activity::requestUpdate(bool immediate) { activityManager.requestUpdate(immediate); } void Activity::requestUpdateAndWait() { // FIXME @ngxson : properly implement this using freeRTOS notification + activityManager.requestUpdate(true); delay(100); } -// RenderLock +void Activity::onGoHome() { activityManager.goHome(); } -Activity::RenderLock::RenderLock(Activity& activity) : activity(activity) { - xSemaphoreTake(activity.renderingMutex, portMAX_DELAY); +void Activity::onSelectBook(const std::string& path) { activityManager.goToReader(path); } + +void Activity::startActivityForResult(std::unique_ptr&& activity, ActivityResultHandler resultHandler) { + this->resultHandler = std::move(resultHandler); + activityManager.pushActivity(std::move(activity)); } -Activity::RenderLock::~RenderLock() { xSemaphoreGive(activity.renderingMutex); } +void Activity::setResult(ActivityResult&& result) { this->result = std::move(result); } + +void Activity::finish() { activityManager.popActivity(); } diff --git a/src/activities/Activity.h b/src/activities/Activity.h index 3669dc46..0d890d97 100644 --- a/src/activities/Activity.h +++ b/src/activities/Activity.h @@ -1,16 +1,16 @@ #pragma once -#include #include -#include -#include -#include #include +#include #include #include +#include "ActivityManager.h" // for using the ActivityManager singleton +#include "ActivityResult.h" #include "GfxRenderer.h" #include "MappedInputManager.h" +#include "RenderLock.h" class Activity { protected: @@ -18,44 +18,42 @@ class Activity { GfxRenderer& renderer; MappedInputManager& mappedInput; - // Task to render and display the activity - TaskHandle_t renderTaskHandle = nullptr; - [[noreturn]] static void renderTaskTrampoline(void* param); - [[noreturn]] virtual void renderTaskLoop(); - - // Mutex to protect rendering operations from being deleted mid-render - SemaphoreHandle_t renderingMutex = nullptr; - public: + ActivityResultHandler resultHandler; + ActivityResult result; + explicit Activity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput) - : name(std::move(name)), renderer(renderer), mappedInput(mappedInput), renderingMutex(xSemaphoreCreateMutex()) { - assert(renderingMutex != nullptr && "Failed to create rendering mutex"); - } - virtual ~Activity() { - vSemaphoreDelete(renderingMutex); - renderingMutex = nullptr; - }; - class RenderLock; + : name(std::move(name)), renderer(renderer), mappedInput(mappedInput) {} + virtual ~Activity() = default; virtual void onEnter(); virtual void onExit(); virtual void loop() {} virtual void render(RenderLock&&) {} - virtual void requestUpdate(); + + // If immediate is true, the update will be triggered immediately. + // Otherwise, it will be deferred until the end of the current loop iteration. + virtual void requestUpdate(bool immediate = false); + + // Request an immediate render and block until it completes. virtual void requestUpdateAndWait(); virtual bool skipLoopDelay() { return false; } virtual bool preventAutoSleep() { return false; } virtual bool isReaderActivity() const { return false; } - // RAII helper to lock rendering mutex for the duration of a scope. - class RenderLock { - Activity& activity; + // Start a new activity without destroying the current one + // Note: requestUpdate() will be invoked automatically once resultHandler finishes + void startActivityForResult(std::unique_ptr&& activity, ActivityResultHandler resultHandler); - public: - explicit RenderLock(Activity& activity); - RenderLock(const RenderLock&) = delete; - RenderLock& operator=(const RenderLock&) = delete; - ~RenderLock(); - }; + // Set the result to be passed back to the previous activity when this activity finishes + void setResult(ActivityResult&& result); + + // Finish this activity and return to the previous one on the stack (if any) + void finish(); + + // Convenience method to facilitate API transition to ActivityManager + // TODO: remove this in near future + void onGoHome(); + void onSelectBook(const std::string& path); }; diff --git a/src/activities/ActivityManager.cpp b/src/activities/ActivityManager.cpp new file mode 100644 index 00000000..0f612dd4 --- /dev/null +++ b/src/activities/ActivityManager.cpp @@ -0,0 +1,252 @@ +#include "ActivityManager.h" + +#include + +#include "boot_sleep/BootActivity.h" +#include "boot_sleep/SleepActivity.h" +#include "browser/OpdsBookBrowserActivity.h" +#include "home/HomeActivity.h" +#include "home/MyLibraryActivity.h" +#include "home/RecentBooksActivity.h" +#include "network/CrossPointWebServerActivity.h" +#include "reader/ReaderActivity.h" +#include "settings/SettingsActivity.h" +#include "util/FullScreenMessageActivity.h" + +void ActivityManager::begin() { + xTaskCreate(&renderTaskTrampoline, "ActivityManagerRender", + 8192, // Stack size + this, // Parameters + 1, // Priority + &renderTaskHandle // Task handle + ); + assert(renderTaskHandle != nullptr && "Failed to create render task"); +} + +void ActivityManager::renderTaskTrampoline(void* param) { + auto* self = static_cast(param); + self->renderTaskLoop(); +} + +void ActivityManager::renderTaskLoop() { + while (true) { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + // Acquire the lock before reading currentActivity to avoid a TOCTOU race + // where the main task deletes the activity between the null-check and render(). + RenderLock lock; + if (currentActivity) { + HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering + currentActivity->render(std::move(lock)); + } + } +} + +void ActivityManager::loop() { + if (currentActivity) { + // Note: do not hold a lock here, the loop() method must be responsible for acquire one if needed + currentActivity->loop(); + } + + while (pendingAction != PendingAction::None) { + if (pendingAction == PendingAction::Pop) { + RenderLock lock; + + if (!currentActivity) { + // Should never happen in practice + LOG_ERR("ACT", "Pop set but currentActivity is null; ignoring pop request"); + pendingAction = PendingAction::None; + continue; + } + + ActivityResult pendingResult = std::move(currentActivity->result); + + // Destroy the current activity + exitActivity(lock); + pendingAction = PendingAction::None; + + if (stackActivities.empty()) { + LOG_DBG("ACT", "No more activities on stack, going home"); + lock.unlock(); // goHome may acquire its own lock + goHome(); + continue; // Will launch goHome immediately + + } else { + currentActivity = std::move(stackActivities.back()); + stackActivities.pop_back(); + LOG_DBG("ACT", "Popped from activity stack, new size = %zu", stackActivities.size()); + // Handle result if necessary + if (currentActivity->resultHandler) { + LOG_DBG("ACT", "Handling result for popped activity"); + + // Move it here to avoid the case where handler calling another startActivityForResult() + auto handler = std::move(currentActivity->resultHandler); + currentActivity->resultHandler = nullptr; + lock.unlock(); // Handler may acquire its own lock + handler(pendingResult); + } + + // Request an update to ensure the popped activity gets re-rendered + if (pendingAction == PendingAction::None) { + requestUpdate(); + } + + // Handler may request another pending action, we will handle it in the next loop iteration + continue; + } + + } else if (pendingActivity) { + // Current activity has requested a new activity to be launched + RenderLock lock; + + if (pendingAction == PendingAction::Replace) { + // Destroy the current activity + exitActivity(lock); + // Clear the stack + while (!stackActivities.empty()) { + stackActivities.back()->onExit(); + stackActivities.pop_back(); + } + } else if (pendingAction == PendingAction::Push) { + // Move current activity to stack + stackActivities.push_back(std::move(currentActivity)); + LOG_DBG("ACT", "Pushed to activity stack, new size = %zu", stackActivities.size()); + } + pendingAction = PendingAction::None; + currentActivity = std::move(pendingActivity); + + lock.unlock(); // onEnter may acquire its own lock + currentActivity->onEnter(); + + // onEnter may request another pending action, we will handle it in the next loop iteration + continue; + } + } + + if (requestedUpdate) { + requestedUpdate = false; + // Using direct notification to signal the render task to update + // Increment counter so multiple rapid calls won't be lost + if (renderTaskHandle) { + xTaskNotify(renderTaskHandle, 1, eIncrement); + } + } +} + +void ActivityManager::exitActivity(const RenderLock& lock) { + // Note: lock must be held by the caller + if (currentActivity) { + currentActivity->onExit(); + currentActivity.reset(); + } +} + +void ActivityManager::replaceActivity(std::unique_ptr&& newActivity) { + // Note: no lock here, this is usually called by loop() and we may run into deadlock + if (currentActivity) { + // Defer launch if we're currently in an activity, to avoid deleting the current activity + // leading to the "delete this" problem + pendingActivity = std::move(newActivity); + pendingAction = PendingAction::Replace; + } else { + // No current activity, safe to launch immediately + currentActivity = std::move(newActivity); + currentActivity->onEnter(); + } +} + +void ActivityManager::goToFileTransfer() { + replaceActivity(std::make_unique(renderer, mappedInput)); +} + +void ActivityManager::goToSettings() { replaceActivity(std::make_unique(renderer, mappedInput)); } + +void ActivityManager::goToMyLibrary(std::string path) { + replaceActivity(std::make_unique(renderer, mappedInput, std::move(path))); +} + +void ActivityManager::goToRecentBooks() { + replaceActivity(std::make_unique(renderer, mappedInput)); +} + +void ActivityManager::goToBrowser() { + replaceActivity(std::make_unique(renderer, mappedInput)); +} + +void ActivityManager::goToReader(std::string path) { + replaceActivity(std::make_unique(renderer, mappedInput, std::move(path))); +} + +void ActivityManager::goToSleep() { + replaceActivity(std::make_unique(renderer, mappedInput)); + loop(); // Important: sleep screen must be rendered immediately, the caller will go to sleep right after this returns +} + +void ActivityManager::goToBoot() { replaceActivity(std::make_unique(renderer, mappedInput)); } + +void ActivityManager::goToFullScreenMessage(std::string message, EpdFontFamily::Style style) { + replaceActivity(std::make_unique(renderer, mappedInput, std::move(message), style)); +} + +void ActivityManager::goHome() { replaceActivity(std::make_unique(renderer, mappedInput)); } + +void ActivityManager::pushActivity(std::unique_ptr&& activity) { + if (pendingActivity) { + // Should never happen in practice + LOG_ERR("ACT", "pendingActivity while pushActivity is not expected"); + pendingActivity.reset(); + } + pendingActivity = std::move(activity); + pendingAction = PendingAction::Push; +} + +void ActivityManager::popActivity() { + if (pendingActivity) { + // Should never happen in practice + LOG_ERR("ACT", "pendingActivity while popActivity is not expected"); + pendingActivity.reset(); + } + pendingAction = PendingAction::Pop; +} + +bool ActivityManager::preventAutoSleep() const { return currentActivity && currentActivity->preventAutoSleep(); } + +bool ActivityManager::isReaderActivity() const { return currentActivity && currentActivity->isReaderActivity(); } + +bool ActivityManager::skipLoopDelay() const { return currentActivity && currentActivity->skipLoopDelay(); } + +void ActivityManager::requestUpdate(bool immediate) { + if (immediate) { + if (renderTaskHandle) { + xTaskNotify(renderTaskHandle, 1, eIncrement); + } + } else { + // Deferring the update until current loop is finished + // This is to avoid multiple updates being requested in the same loop + requestedUpdate = true; + } +} +// RenderLock + +RenderLock::RenderLock() { + xSemaphoreTake(activityManager.renderingMutex, portMAX_DELAY); + isLocked = true; +} + +RenderLock::RenderLock(Activity& /* unused */) { + xSemaphoreTake(activityManager.renderingMutex, portMAX_DELAY); + isLocked = true; +} + +RenderLock::~RenderLock() { + if (isLocked) { + xSemaphoreGive(activityManager.renderingMutex); + isLocked = false; + } +} + +void RenderLock::unlock() { + if (isLocked) { + xSemaphoreGive(activityManager.renderingMutex); + isLocked = false; + } +} diff --git a/src/activities/ActivityManager.h b/src/activities/ActivityManager.h new file mode 100644 index 00000000..698b9a7a --- /dev/null +++ b/src/activities/ActivityManager.h @@ -0,0 +1,102 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +#include "GfxRenderer.h" +#include "MappedInputManager.h" +#include "RenderLock.h" + +class Activity; // forward declaration +class RenderLock; // forward declaration + +/** + * ActivityManager + * + * This mirrors the same concept of Activity in Android, where an activity represents a single screen of the UI. The + * manager is responsible for launching activities, and ensuring that only one activity is active at a time. + * + * It also provides a stack mechanism to allow activities to launch sub-activities and get back the results when the + * sub-activity is done. For example, the WebServer activity can launch a WifiSelect activity to let the user choose a + * wifi network, and get back the selected network when the user is done. + * + * Main differences from Android's ActivityManager: + * - No onPause/onResume, since we don't have a concept of background activities + * - onActivityResult is implemented via a callback instead of a separate method, for simplicity + */ +class ActivityManager { + protected: + GfxRenderer& renderer; + MappedInputManager& mappedInput; + std::vector> stackActivities; + std::unique_ptr currentActivity; + + void exitActivity(const RenderLock& lock); + + // Pending activity to be launched on next loop iteration + std::unique_ptr pendingActivity; + enum class PendingAction { None, Push, Pop, Replace }; + PendingAction pendingAction = PendingAction::None; + + // Task to render and display the activity + TaskHandle_t renderTaskHandle = nullptr; + static void renderTaskTrampoline(void* param); + [[noreturn]] virtual void renderTaskLoop(); + + // Whether to trigger a render after the current loop() + // This variable must only be set by the main loop, to avoid race conditions + bool requestedUpdate = false; + + public: + explicit ActivityManager(GfxRenderer& renderer, MappedInputManager& mappedInput) + : renderer(renderer), mappedInput(mappedInput), renderingMutex(xSemaphoreCreateMutex()) { + assert(renderingMutex != nullptr && "Failed to create rendering mutex"); + stackActivities.reserve(10); + } + ~ActivityManager() { assert(false); /* should never be called */ }; + + // Mutex to protect rendering operations from race conditions + // Must only be used via RenderLock + SemaphoreHandle_t renderingMutex = nullptr; + + void begin(); + void loop(); + + // Will replace currentActivity and drop all activities on stack + void replaceActivity(std::unique_ptr&& newActivity); + + // goTo... functions are convenient wrapper for replaceActivity() + void goToFileTransfer(); + void goToSettings(); + void goToMyLibrary(std::string path = {}); + void goToRecentBooks(); + void goToBrowser(); + void goToReader(std::string path); + void goToSleep(); + void goToBoot(); + void goToFullScreenMessage(std::string message, EpdFontFamily::Style style = EpdFontFamily::REGULAR); + void goHome(); + + // This will move current activity to stack instead of deleting it + void pushActivity(std::unique_ptr&& activity); + + // Remove the currentActivity, returning the last one on stack + // Note: if popActivity() on last activity on the stack, we will goHome() + void popActivity(); + + bool preventAutoSleep() const; + bool isReaderActivity() const; + bool skipLoopDelay() const; + + // If immediate is true, the update will be triggered immediately. + // Otherwise, it will be deferred until the end of the current loop iteration. + void requestUpdate(bool immediate = false); +}; + +extern ActivityManager activityManager; // singleton, to be defined in main.cpp diff --git a/src/activities/ActivityResult.h b/src/activities/ActivityResult.h new file mode 100644 index 00000000..dc947004 --- /dev/null +++ b/src/activities/ActivityResult.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +struct WifiResult { + bool connected = false; + std::string ssid; + std::string ip; +}; + +struct KeyboardResult { + std::string text; +}; + +struct MenuResult { + int action = -1; + uint8_t orientation = 0; +}; + +struct ChapterResult { + int spineIndex = 0; +}; + +struct PercentResult { + int percent = 0; +}; + +struct PageResult { + uint32_t page = 0; +}; + +struct SyncResult { + int spineIndex = 0; + int page = 0; +}; + +enum class NetworkMode; + +struct NetworkModeResult { + NetworkMode mode; +}; + +struct FootnoteResult { + std::string href; +}; + +using ResultVariant = std::variant; + +struct ActivityResult { + bool isCancelled = false; + ResultVariant data; + + explicit ActivityResult() = default; + + template >> + // cppcheck-suppress noExplicitConstructor + ActivityResult(ResultType&& result) : data{std::forward(result)} {} +}; + +using ActivityResultHandler = std::function; diff --git a/src/activities/ActivityWithSubactivity.cpp b/src/activities/ActivityWithSubactivity.cpp deleted file mode 100644 index 2af75d73..00000000 --- a/src/activities/ActivityWithSubactivity.cpp +++ /dev/null @@ -1,53 +0,0 @@ -#include "ActivityWithSubactivity.h" - -#include - -void ActivityWithSubactivity::renderTaskLoop() { - while (true) { - ulTaskNotifyTake(pdTRUE, portMAX_DELAY); - { - HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering - RenderLock lock(*this); - if (!subActivity) { - render(std::move(lock)); - } - // If subActivity is set, consume the notification but skip parent render - // Note: the sub-activity will call its render() from its own display task - } - } -} - -void ActivityWithSubactivity::exitActivity() { - // No need to lock, since onExit() already acquires its own lock - if (subActivity) { - LOG_DBG("ACT", "Exiting subactivity..."); - subActivity->onExit(); - subActivity.reset(); - } -} - -void ActivityWithSubactivity::enterNewActivity(Activity* activity) { - // Acquire lock to avoid 2 activities rendering at the same time during transition - RenderLock lock(*this); - subActivity.reset(activity); - subActivity->onEnter(); -} - -void ActivityWithSubactivity::loop() { - if (subActivity) { - subActivity->loop(); - } -} - -void ActivityWithSubactivity::requestUpdate() { - if (!subActivity) { - Activity::requestUpdate(); - } - // Sub-activity should call their own requestUpdate() from their loop() function -} - -void ActivityWithSubactivity::onExit() { - // No need to lock, onExit() already acquires its own lock - exitActivity(); - Activity::onExit(); -} diff --git a/src/activities/ActivityWithSubactivity.h b/src/activities/ActivityWithSubactivity.h deleted file mode 100644 index 38329a2a..00000000 --- a/src/activities/ActivityWithSubactivity.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once -#include - -#include "Activity.h" - -class ActivityWithSubactivity : public Activity { - protected: - std::unique_ptr subActivity = nullptr; - void exitActivity(); - void enterNewActivity(Activity* activity); - [[noreturn]] void renderTaskLoop() override; - - public: - explicit ActivityWithSubactivity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity(std::move(name), renderer, mappedInput) {} - void loop() override; - // Note: when a subactivity is active, parent requestUpdate() calls are ignored; - // the subactivity should request its own renders. This pauses parent rendering until exit. - void requestUpdate() override; - void onExit() override; -}; diff --git a/src/activities/RenderLock.h b/src/activities/RenderLock.h new file mode 100644 index 00000000..eeda2cf6 --- /dev/null +++ b/src/activities/RenderLock.h @@ -0,0 +1,16 @@ +#pragma once + +class Activity; // forward declaration + +// RAII helper to lock rendering mutex for the duration of a scope. +class RenderLock { + bool isLocked = false; + + public: + explicit RenderLock(); + explicit RenderLock(Activity&); // unused for now, but keep for compatibility + RenderLock(const RenderLock&) = delete; + RenderLock& operator=(const RenderLock&) = delete; + ~RenderLock(); + void unlock(); +}; diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index ed9cc29b..62aeab38 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -21,7 +21,7 @@ constexpr int PAGE_ITEMS = 23; } // namespace void OpdsBookBrowserActivity::onEnter() { - ActivityWithSubactivity::onEnter(); + Activity::onEnter(); state = BrowserState::CHECK_WIFI; entries.clear(); @@ -37,7 +37,7 @@ void OpdsBookBrowserActivity::onEnter() { } void OpdsBookBrowserActivity::onExit() { - ActivityWithSubactivity::onExit(); + Activity::onExit(); // Turn off WiFi when exiting WiFi.mode(WIFI_OFF); @@ -49,7 +49,7 @@ void OpdsBookBrowserActivity::onExit() { void OpdsBookBrowserActivity::loop() { // Handle WiFi selection subactivity if (state == BrowserState::WIFI_SELECTION) { - ActivityWithSubactivity::loop(); + // Should already handled by the WifiSelectionActivity return; } @@ -136,7 +136,7 @@ void OpdsBookBrowserActivity::loop() { } } -void OpdsBookBrowserActivity::render(Activity::RenderLock&&) { +void OpdsBookBrowserActivity::render(RenderLock&&) { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); @@ -279,7 +279,7 @@ void OpdsBookBrowserActivity::navigateToEntry(const OpdsEntry& entry) { statusMessage = tr(STR_LOADING); entries.clear(); selectorIndex = 0; - requestUpdate(); + requestUpdate(true); // Force update to show loading state immediately before fetch fetchFeed(currentPath); } @@ -308,7 +308,7 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) { statusMessage = book.title; downloadProgress = 0; downloadTotal = 0; - requestUpdate(); + requestUpdate(true); // Build full download URL std::string downloadUrl = UrlUtils::buildUrl(SETTINGS.opdsServerUrl, book.href); @@ -326,7 +326,7 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) { HttpDownloader::downloadToFile(downloadUrl, filename, [this](const size_t downloaded, const size_t total) { downloadProgress = downloaded; downloadTotal = total; - requestUpdate(); + requestUpdate(true); // Force update to refresh progress bar }); if (result == HttpDownloader::OK) { @@ -364,18 +364,16 @@ void OpdsBookBrowserActivity::launchWifiSelection() { state = BrowserState::WIFI_SELECTION; requestUpdate(); - enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, - [this](const bool connected) { onWifiSelectionComplete(connected); })); + startActivityForResult(std::make_unique(renderer, mappedInput), + [this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); }); } void OpdsBookBrowserActivity::onWifiSelectionComplete(const bool connected) { - exitActivity(); - if (connected) { LOG_DBG("OPDS", "WiFi connected via selection, fetching feed"); state = BrowserState::LOADING; statusMessage = tr(STR_LOADING); - requestUpdate(); + requestUpdate(true); // Force update to show loading state immediately before fetch fetchFeed(currentPath); } else { LOG_DBG("OPDS", "WiFi selection cancelled/failed"); @@ -385,6 +383,5 @@ void OpdsBookBrowserActivity::onWifiSelectionComplete(const bool connected) { WiFi.mode(WIFI_OFF); state = BrowserState::ERROR; errorMessage = tr(STR_WIFI_CONN_FAILED); - requestUpdate(); } } diff --git a/src/activities/browser/OpdsBookBrowserActivity.h b/src/activities/browser/OpdsBookBrowserActivity.h index 3ee94f0a..fa716cbd 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.h +++ b/src/activities/browser/OpdsBookBrowserActivity.h @@ -5,7 +5,7 @@ #include #include -#include "../ActivityWithSubactivity.h" +#include "../Activity.h" #include "util/ButtonNavigator.h" /** @@ -13,7 +13,7 @@ * Supports navigation through catalog hierarchy and downloading EPUBs. * When WiFi connection fails, launches WiFi selection to let user connect. */ -class OpdsBookBrowserActivity final : public ActivityWithSubactivity { +class OpdsBookBrowserActivity final : public Activity { public: enum class BrowserState { CHECK_WIFI, // Checking WiFi connection @@ -24,14 +24,13 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity { ERROR // Error state with message }; - explicit OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onGoHome) - : ActivityWithSubactivity("OpdsBookBrowser", renderer, mappedInput), onGoHome(onGoHome) {} + explicit OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("OpdsBookBrowser", renderer, mappedInput) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; private: ButtonNavigator buttonNavigator; @@ -45,8 +44,6 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity { size_t downloadProgress = 0; size_t downloadTotal = 0; - const std::function onGoHome; - void checkAndConnectWifi(); void launchWifiSelection(); void onWifiSelectionComplete(bool connected); diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index dc0d47be..6ee6d74d 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -211,7 +211,7 @@ void HomeActivity::loop() { } } -void HomeActivity::render(Activity::RenderLock&&) { +void HomeActivity::render(RenderLock&&) { const auto& metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); @@ -258,3 +258,15 @@ void HomeActivity::render(Activity::RenderLock&&) { loadRecentCovers(metrics.homeCoverHeight); } } + +void HomeActivity::onSelectBook(const std::string& path) { activityManager.goToReader(path); } + +void HomeActivity::onMyLibraryOpen() { activityManager.goToMyLibrary(); } + +void HomeActivity::onRecentsOpen() { activityManager.goToRecentBooks(); } + +void HomeActivity::onSettingsOpen() { activityManager.goToSettings(); } + +void HomeActivity::onFileTransferOpen() { activityManager.goToFileTransfer(); } + +void HomeActivity::onOpdsBrowserOpen() { activityManager.goToBrowser(); } diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index 5359bf79..6b69d516 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -20,12 +20,12 @@ class HomeActivity final : public Activity { bool coverBufferStored = false; // Track if cover buffer is stored uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image std::vector recentBooks; - const std::function onSelectBook; - const std::function onMyLibraryOpen; - const std::function onRecentsOpen; - const std::function onSettingsOpen; - const std::function onFileTransferOpen; - const std::function onOpdsBrowserOpen; + void onSelectBook(const std::string& path); + void onMyLibraryOpen(); + void onRecentsOpen(); + void onSettingsOpen(); + void onFileTransferOpen(); + void onOpdsBrowserOpen(); int getMenuItemCount() const; bool storeCoverBuffer(); // Store frame buffer for cover image @@ -35,20 +35,10 @@ class HomeActivity final : public Activity { void loadRecentCovers(int coverHeight); public: - explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onSelectBook, - const std::function& onMyLibraryOpen, const std::function& onRecentsOpen, - const std::function& onSettingsOpen, const std::function& onFileTransferOpen, - const std::function& onOpdsBrowserOpen) - : Activity("Home", renderer, mappedInput), - onSelectBook(onSelectBook), - onMyLibraryOpen(onMyLibraryOpen), - onRecentsOpen(onRecentsOpen), - onSettingsOpen(onSettingsOpen), - onFileTransferOpen(onFileTransferOpen), - onOpdsBrowserOpen(onOpdsBrowserOpen) {} + explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("Home", renderer, mappedInput) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; }; diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 56799e91..84428153 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -196,7 +196,7 @@ std::string getFileName(std::string filename) { return filename.substr(0, pos); } -void MyLibraryActivity::render(Activity::RenderLock&&) { +void MyLibraryActivity::render(RenderLock&&) { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); diff --git a/src/activities/home/MyLibraryActivity.h b/src/activities/home/MyLibraryActivity.h index 748b7eea..6bc1faaa 100644 --- a/src/activities/home/MyLibraryActivity.h +++ b/src/activities/home/MyLibraryActivity.h @@ -17,25 +17,15 @@ class MyLibraryActivity final : public Activity { std::string basepath = "/"; std::vector files; - // Callbacks - const std::function onSelectBook; - const std::function onGoHome; - // Data loading void loadFiles(); size_t findEntry(const std::string& name) const; public: - explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onGoHome, - const std::function& onSelectBook, - std::string initialPath = "/") - : Activity("MyLibrary", renderer, mappedInput), - basepath(initialPath.empty() ? "/" : std::move(initialPath)), - onSelectBook(onSelectBook), - onGoHome(onGoHome) {} + explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialPath = "/") + : Activity("MyLibrary", renderer, mappedInput), basepath(initialPath.empty() ? "/" : std::move(initialPath)) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; }; diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index 0b519d31..e3e611fb 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -83,7 +83,7 @@ void RecentBooksActivity::loop() { }); } -void RecentBooksActivity::render(Activity::RenderLock&&) { +void RecentBooksActivity::render(RenderLock&&) { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); diff --git a/src/activities/home/RecentBooksActivity.h b/src/activities/home/RecentBooksActivity.h index 9ad44213..b1103f04 100644 --- a/src/activities/home/RecentBooksActivity.h +++ b/src/activities/home/RecentBooksActivity.h @@ -18,20 +18,14 @@ class RecentBooksActivity final : public Activity { // Recent tab state std::vector recentBooks; - // Callbacks - const std::function onSelectBook; - const std::function onGoHome; - // Data loading void loadRecentBooks(); public: - explicit RecentBooksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onGoHome, - const std::function& onSelectBook) - : Activity("RecentBooks", renderer, mappedInput), onSelectBook(onSelectBook), onGoHome(onGoHome) {} + explicit RecentBooksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("RecentBooks", renderer, mappedInput) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; }; diff --git a/src/activities/network/CalibreConnectActivity.cpp b/src/activities/network/CalibreConnectActivity.cpp index fbf9dd04..4933178a 100644 --- a/src/activities/network/CalibreConnectActivity.cpp +++ b/src/activities/network/CalibreConnectActivity.cpp @@ -16,7 +16,7 @@ constexpr const char* HOSTNAME = "crosspoint"; } // namespace void CalibreConnectActivity::onEnter() { - ActivityWithSubactivity::onEnter(); + Activity::onEnter(); requestUpdate(); state = CalibreConnectState::WIFI_SELECTION; @@ -32,8 +32,15 @@ void CalibreConnectActivity::onEnter() { exitRequested = false; if (WiFi.status() != WL_CONNECTED) { - enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, - [this](const bool connected) { onWifiSelectionComplete(connected); })); + startActivityForResult(std::make_unique(renderer, mappedInput), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& wifi = std::get(result.data); + connectedIP = wifi.ip; + connectedSSID = wifi.ssid; + } + onWifiSelectionComplete(!result.isCancelled); + }); } else { connectedIP = WiFi.localIP().toString().c_str(); connectedSSID = WiFi.SSID().c_str(); @@ -42,7 +49,7 @@ void CalibreConnectActivity::onEnter() { } void CalibreConnectActivity::onExit() { - ActivityWithSubactivity::onExit(); + Activity::onExit(); stopWebServer(); MDNS.end(); @@ -56,18 +63,10 @@ void CalibreConnectActivity::onExit() { void CalibreConnectActivity::onWifiSelectionComplete(const bool connected) { if (!connected) { - exitActivity(); - onComplete(); + activityManager.popActivity(); return; } - if (subActivity) { - connectedIP = static_cast(subActivity.get())->getConnectedIP(); - } else { - connectedIP = WiFi.localIP().toString().c_str(); - } - connectedSSID = WiFi.SSID().c_str(); - exitActivity(); startWebServer(); } @@ -100,11 +99,6 @@ void CalibreConnectActivity::stopWebServer() { } void CalibreConnectActivity::loop() { - if (subActivity) { - subActivity->loop(); - return; - } - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { exitRequested = true; } @@ -168,12 +162,12 @@ void CalibreConnectActivity::loop() { } if (exitRequested) { - onComplete(); + activityManager.popActivity(); return; } } -void CalibreConnectActivity::render(Activity::RenderLock&&) { +void CalibreConnectActivity::render(RenderLock&&) { const auto& metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); diff --git a/src/activities/network/CalibreConnectActivity.h b/src/activities/network/CalibreConnectActivity.h index 451d3bc9..a8afd4af 100644 --- a/src/activities/network/CalibreConnectActivity.h +++ b/src/activities/network/CalibreConnectActivity.h @@ -4,7 +4,7 @@ #include #include -#include "activities/ActivityWithSubactivity.h" +#include "activities/Activity.h" #include "network/CrossPointWebServer.h" enum class CalibreConnectState { WIFI_SELECTION, SERVER_STARTING, SERVER_RUNNING, ERROR }; @@ -13,9 +13,8 @@ enum class CalibreConnectState { WIFI_SELECTION, SERVER_STARTING, SERVER_RUNNING * CalibreConnectActivity starts the file transfer server in STA mode, * but renders Calibre-specific instructions instead of the web transfer UI. */ -class CalibreConnectActivity final : public ActivityWithSubactivity { +class CalibreConnectActivity final : public Activity { CalibreConnectState state = CalibreConnectState::WIFI_SELECTION; - const std::function onComplete; std::unique_ptr webServer; std::string connectedIP; @@ -36,13 +35,12 @@ class CalibreConnectActivity final : public ActivityWithSubactivity { void stopWebServer(); public: - explicit CalibreConnectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onComplete) - : ActivityWithSubactivity("CalibreConnect", renderer, mappedInput), onComplete(onComplete) {} + explicit CalibreConnectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("CalibreConnect", renderer, mappedInput) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; bool skipLoopDelay() override { return webServer && webServer->isRunning(); } bool preventAutoSleep() override { return webServer && webServer->isRunning(); } }; diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index 7e77cbe4..b3cc8642 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -33,7 +33,7 @@ constexpr uint16_t DNS_PORT = 53; } // namespace void CrossPointWebServerActivity::onEnter() { - ActivityWithSubactivity::onEnter(); + Activity::onEnter(); LOG_DBG("WEBACT", "Free heap at onEnter: %d bytes", ESP.getFreeHeap()); @@ -48,14 +48,18 @@ void CrossPointWebServerActivity::onEnter() { // Launch network mode selection subactivity LOG_DBG("WEBACT", "Launching NetworkModeSelectionActivity..."); - enterNewActivity(new NetworkModeSelectionActivity( - renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); }, - [this]() { onGoBack(); } // Cancel goes back to home - )); + startActivityForResult(std::make_unique(renderer, mappedInput), + [this](const ActivityResult& result) { + if (result.isCancelled) { + onGoHome(); + } else { + onNetworkModeSelected(std::get(result.data).mode); + } + }); } void CrossPointWebServerActivity::onExit() { - ActivityWithSubactivity::onExit(); + Activity::onExit(); LOG_DBG("WEBACT", "Free heap at onExit start: %d bytes", ESP.getFreeHeap()); @@ -107,18 +111,20 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode) networkMode = mode; isApMode = (mode == NetworkMode::CREATE_HOTSPOT); - // Exit mode selection subactivity - exitActivity(); - if (mode == NetworkMode::CONNECT_CALIBRE) { - exitActivity(); - enterNewActivity(new CalibreConnectActivity(renderer, mappedInput, [this] { - exitActivity(); - state = WebServerActivityState::MODE_SELECTION; - enterNewActivity(new NetworkModeSelectionActivity( - renderer, mappedInput, [this](const NetworkMode nextMode) { onNetworkModeSelected(nextMode); }, - [this]() { onGoBack(); })); - })); + startActivityForResult( + std::make_unique(renderer, mappedInput), [this](const ActivityResult& result) { + state = WebServerActivityState::MODE_SELECTION; + + startActivityForResult(std::make_unique(renderer, mappedInput), + [this](const ActivityResult& result) { + if (result.isCancelled) { + onGoHome(); + } else { + onNetworkModeSelected(std::get(result.data).mode); + } + }); + }); return; } @@ -129,8 +135,15 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode) state = WebServerActivityState::WIFI_SELECTION; LOG_DBG("WEBACT", "Launching WifiSelectionActivity..."); - enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, - [this](const bool connected) { onWifiSelectionComplete(connected); })); + startActivityForResult(std::make_unique(renderer, mappedInput), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& wifi = std::get(result.data); + connectedIP = wifi.ip; + connectedSSID = wifi.ssid; + } + onWifiSelectionComplete(!result.isCancelled); + }); } else { // AP mode - start access point state = WebServerActivityState::AP_STARTING; @@ -144,12 +157,8 @@ void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected) if (connected) { // Get connection info before exiting subactivity - connectedIP = static_cast(subActivity.get())->getConnectedIP(); - connectedSSID = WiFi.SSID().c_str(); isApMode = false; - exitActivity(); - // Start mDNS for hostname resolution if (MDNS.begin(AP_HOSTNAME)) { LOG_DBG("WEBACT", "mDNS started: http://%s.local/", AP_HOSTNAME); @@ -159,11 +168,16 @@ void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected) startWebServer(); } else { // User cancelled - go back to mode selection - exitActivity(); state = WebServerActivityState::MODE_SELECTION; - enterNewActivity(new NetworkModeSelectionActivity( - renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); }, - [this]() { onGoBack(); })); + + startActivityForResult(std::make_unique(renderer, mappedInput), + [this](const ActivityResult& result) { + if (result.isCancelled) { + onGoHome(); + } else { + onNetworkModeSelected(std::get(result.data).mode); + } + }); } } @@ -186,7 +200,7 @@ void CrossPointWebServerActivity::startAccessPoint() { if (!apStarted) { LOG_ERR("WEBACT", "ERROR: Failed to start Access Point!"); - onGoBack(); + onGoHome(); return; } @@ -236,16 +250,12 @@ void CrossPointWebServerActivity::startWebServer() { // Force an immediate render since we're transitioning from a subactivity // that had its own rendering task. We need to make sure our display is shown. - { - RenderLock lock(*this); - render(std::move(lock)); - } - LOG_DBG("WEBACT", "Rendered File Transfer screen"); + requestUpdate(); } else { LOG_ERR("WEBACT", "ERROR: Failed to start web server!"); webServer.reset(); // Go back on error - onGoBack(); + onGoHome(); } } @@ -259,12 +269,6 @@ void CrossPointWebServerActivity::stopWebServer() { } void CrossPointWebServerActivity::loop() { - if (subActivity) { - // Forward loop to subactivity - subActivity->loop(); - return; - } - // Handle different states if (state == WebServerActivityState::SERVER_RUNNING) { // Handle DNS requests for captive portal (AP mode only) @@ -322,7 +326,7 @@ void CrossPointWebServerActivity::loop() { mappedInput.update(); // Check for exit button inside loop for responsiveness if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { - onGoBack(); + onGoHome(); return; } } @@ -332,13 +336,13 @@ void CrossPointWebServerActivity::loop() { // Handle exit on Back button (also check outside loop) if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { - onGoBack(); + onGoHome(); return; } } } -void CrossPointWebServerActivity::render(Activity::RenderLock&&) { +void CrossPointWebServerActivity::render(RenderLock&&) { // Only render our own UI when server is running // Subactivities handle their own rendering if (state == WebServerActivityState::SERVER_RUNNING || state == WebServerActivityState::AP_STARTING) { diff --git a/src/activities/network/CrossPointWebServerActivity.h b/src/activities/network/CrossPointWebServerActivity.h index 145c11ea..5069f199 100644 --- a/src/activities/network/CrossPointWebServerActivity.h +++ b/src/activities/network/CrossPointWebServerActivity.h @@ -5,7 +5,7 @@ #include #include "NetworkModeSelectionActivity.h" -#include "activities/ActivityWithSubactivity.h" +#include "activities/Activity.h" #include "network/CrossPointWebServer.h" // Web server activity states @@ -27,9 +27,8 @@ enum class WebServerActivityState { * - Handles client requests in its loop() function * - Cleans up the server and shuts down WiFi on exit */ -class CrossPointWebServerActivity final : public ActivityWithSubactivity { +class CrossPointWebServerActivity final : public Activity { WebServerActivityState state = WebServerActivityState::MODE_SELECTION; - const std::function onGoBack; // Network mode NetworkMode networkMode = NetworkMode::JOIN_NETWORK; @@ -54,13 +53,12 @@ class CrossPointWebServerActivity final : public ActivityWithSubactivity { void stopWebServer(); public: - explicit CrossPointWebServerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onGoBack) - : ActivityWithSubactivity("CrossPointWebServer", renderer, mappedInput), onGoBack(onGoBack) {} + explicit CrossPointWebServerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("CrossPointWebServer", renderer, mappedInput) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; bool skipLoopDelay() override { return webServer && webServer->isRunning(); } bool preventAutoSleep() override { return webServer && webServer->isRunning(); } }; diff --git a/src/activities/network/NetworkModeSelectionActivity.cpp b/src/activities/network/NetworkModeSelectionActivity.cpp index 9bba7552..f8e45307 100644 --- a/src/activities/network/NetworkModeSelectionActivity.cpp +++ b/src/activities/network/NetworkModeSelectionActivity.cpp @@ -54,7 +54,7 @@ void NetworkModeSelectionActivity::loop() { }); } -void NetworkModeSelectionActivity::render(Activity::RenderLock&&) { +void NetworkModeSelectionActivity::render(RenderLock&&) { renderer.clearScreen(); const auto& metrics = UITheme::getInstance().getMetrics(); @@ -83,3 +83,15 @@ void NetworkModeSelectionActivity::render(Activity::RenderLock&&) { renderer.displayBuffer(); } + +void NetworkModeSelectionActivity::onModeSelected(NetworkMode mode) { + setResult(NetworkModeResult{mode}); + finish(); +} + +void NetworkModeSelectionActivity::onCancel() { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); +} diff --git a/src/activities/network/NetworkModeSelectionActivity.h b/src/activities/network/NetworkModeSelectionActivity.h index 89fe3499..a119bc1b 100644 --- a/src/activities/network/NetworkModeSelectionActivity.h +++ b/src/activities/network/NetworkModeSelectionActivity.h @@ -5,7 +5,6 @@ #include "../Activity.h" #include "util/ButtonNavigator.h" -// Enum for network mode selection enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT }; /** @@ -22,16 +21,14 @@ class NetworkModeSelectionActivity final : public Activity { int selectedIndex = 0; - const std::function onModeSelected; - const std::function onCancel; - public: - explicit NetworkModeSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onModeSelected, - const std::function& onCancel) - : Activity("NetworkModeSelection", renderer, mappedInput), onModeSelected(onModeSelected), onCancel(onCancel) {} + explicit NetworkModeSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("NetworkModeSelection", renderer, mappedInput) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; + + void onModeSelected(NetworkMode mode); + void onCancel(); }; diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index 43a7292a..06ab788f 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -190,20 +190,19 @@ void WifiSelectionActivity::selectNetwork(const int index) { // Show password entry state = WifiSelectionState::PASSWORD_ENTRY; // Don't allow screen updates while changing activity - enterNewActivity(new KeyboardEntryActivity( - renderer, mappedInput, tr(STR_ENTER_WIFI_PASSWORD), - "", // No initial text - 64, // Max password length - false, // Show password by default (hard keyboard to use) - [this](const std::string& text) { - enteredPassword = text; - exitActivity(); - }, - [this] { - state = WifiSelectionState::NETWORK_LIST; - exitActivity(); - requestUpdate(); - })); + startActivityForResult( + std::make_unique(renderer, mappedInput, tr(STR_ENTER_WIFI_PASSWORD), + "", // No initial text + 64, // Max password length + false // Show password by default (hard keyboard to use) + ), + [this](const ActivityResult& result) { + if (result.isCancelled) { + state = WifiSelectionState::NETWORK_LIST; + } else { + enteredPassword = std::get(result.data).text; + } + }); } else { // Connect directly for open networks attemptConnection(); @@ -291,11 +290,6 @@ void WifiSelectionActivity::checkConnectionStatus() { } void WifiSelectionActivity::loop() { - if (subActivity) { - subActivity->loop(); - return; - } - // Check scan progress if (state == WifiSelectionState::SCANNING) { processWifiScanResults(); @@ -467,7 +461,7 @@ std::string WifiSelectionActivity::getSignalStrengthIndicator(const int32_t rssi return " |"; // Very weak } -void WifiSelectionActivity::render(Activity::RenderLock&&) { +void WifiSelectionActivity::render(RenderLock&&) { // Don't render if we're in PASSWORD_ENTRY state - we're just transitioning // from the keyboard subactivity back to the main activity if (state == WifiSelectionState::PASSWORD_ENTRY) { @@ -693,3 +687,13 @@ void WifiSelectionActivity::renderForgetPrompt() const { const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_LEFT), tr(STR_DIR_RIGHT)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } + +void WifiSelectionActivity::onComplete(const bool connected) { + ActivityResult result; + result.isCancelled = !connected; + if (connected) { + result.data = WifiResult{true, selectedSSID, connectedIP}; + } + setResult(std::move(result)); + finish(); +} diff --git a/src/activities/network/WifiSelectionActivity.h b/src/activities/network/WifiSelectionActivity.h index 9356383c..1ea7fc0a 100644 --- a/src/activities/network/WifiSelectionActivity.h +++ b/src/activities/network/WifiSelectionActivity.h @@ -6,7 +6,7 @@ #include #include -#include "activities/ActivityWithSubactivity.h" +#include "activities/Activity.h" #include "util/ButtonNavigator.h" // Structure to hold WiFi network information @@ -15,6 +15,7 @@ struct WifiNetworkInfo { int32_t rssi; bool isEncrypted; bool hasSavedPassword; // Whether we have saved credentials for this network + std::string ipAddress; // Populated after connection for display }; // WiFi selection states @@ -41,13 +42,12 @@ enum class WifiSelectionState { * * The onComplete callback receives true if connected successfully, false if cancelled. */ -class WifiSelectionActivity final : public ActivityWithSubactivity { +class WifiSelectionActivity final : public Activity { ButtonNavigator buttonNavigator; WifiSelectionState state = WifiSelectionState::SCANNING; size_t selectedNetworkIndex = 0; std::vector networks; - const std::function onComplete; // Selected network for connection std::string selectedSSID; @@ -95,17 +95,13 @@ class WifiSelectionActivity final : public ActivityWithSubactivity { void checkConnectionStatus(); std::string getSignalStrengthIndicator(int32_t rssi) const; + void onComplete(bool connected); + public: - explicit WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onComplete, bool autoConnect = true) - : ActivityWithSubactivity("WifiSelection", renderer, mappedInput), - onComplete(onComplete), - allowAutoConnect(autoConnect) {} + explicit WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, bool autoConnect = true) + : Activity("WifiSelection", renderer, mappedInput), allowAutoConnect(autoConnect) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; - - // Get the IP address after successful connection - const std::string& getConnectedIP() const { return connectedIP; } + void render(RenderLock&&) override; }; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 9a36be00..f5c54cda 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -61,7 +61,7 @@ void applyReaderOrientation(GfxRenderer& renderer, const uint8_t orientation) { } // namespace void EpubReaderActivity::onEnter() { - ActivityWithSubactivity::onEnter(); + Activity::onEnter(); if (!epub) { return; @@ -108,7 +108,7 @@ void EpubReaderActivity::onEnter() { } void EpubReaderActivity::onExit() { - ActivityWithSubactivity::onExit(); + Activity::onExit(); // Reset orientation back to portrait for the rest of the UI renderer.setOrientation(GfxRenderer::Orientation::Portrait); @@ -120,48 +120,9 @@ void EpubReaderActivity::onExit() { } void EpubReaderActivity::loop() { - // Pass input responsibility to sub activity if exists - if (subActivity) { - subActivity->loop(); - // Deferred exit: process after subActivity->loop() returns to avoid use-after-free - if (pendingSubactivityExit) { - pendingSubactivityExit = false; - exitActivity(); - requestUpdate(); - skipNextButtonCheck = true; // Skip button processing to ignore stale events - } - // Deferred go home: process after subActivity->loop() returns to avoid race condition - if (pendingGoHome) { - pendingGoHome = false; - exitActivity(); - if (onGoHome) { - onGoHome(); - } - return; // Don't access 'this' after callback - } - return; - } - - // Handle pending go home when no subactivity (e.g., from long press back) - if (pendingGoHome) { - pendingGoHome = false; - if (onGoHome) { - onGoHome(); - } - return; // Don't access 'this' after callback - } - - // Skip button processing after returning from subactivity - // This prevents stale button release events from triggering actions - // We wait until: (1) all relevant buttons are released, AND (2) wasReleased events have been cleared - if (skipNextButtonCheck) { - const bool confirmCleared = !mappedInput.isPressed(MappedInputManager::Button::Confirm) && - !mappedInput.wasReleased(MappedInputManager::Button::Confirm); - const bool backCleared = !mappedInput.isPressed(MappedInputManager::Button::Back) && - !mappedInput.wasReleased(MappedInputManager::Button::Back); - if (confirmCleared && backCleared) { - skipNextButtonCheck = false; - } + if (!epub) { + // Should never happen + finish(); return; } @@ -170,22 +131,27 @@ void EpubReaderActivity::loop() { const int currentPage = section ? section->currentPage + 1 : 0; const int totalPages = section ? section->pageCount : 0; float bookProgress = 0.0f; - if (epub && epub->getBookSize() > 0 && section && section->pageCount > 0) { + if (epub->getBookSize() > 0 && section && section->pageCount > 0) { const float chapterProgress = static_cast(section->currentPage) / static_cast(section->pageCount); bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; } const int bookProgressPercent = clampPercent(static_cast(bookProgress + 0.5f)); - exitActivity(); - enterNewActivity(new EpubReaderMenuActivity( - this->renderer, this->mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, - SETTINGS.orientation, !currentPageFootnotes.empty(), - [this](const uint8_t orientation) { onReaderMenuBack(orientation); }, - [this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); })); + startActivityForResult(std::make_unique( + renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, + SETTINGS.orientation, !currentPageFootnotes.empty()), + [this](const ActivityResult& result) { + // Always apply orientation change even if the menu was cancelled + const auto& menu = std::get(result.data); + applyOrientation(menu.orientation); + if (!result.isCancelled) { + onReaderMenuConfirm(static_cast(menu.action)); + } + }); } // Long press BACK (1s+) goes to file selection if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) { - onGoBack(); + activityManager.goToMyLibrary(epub ? epub->getPath() : ""); return; } @@ -274,14 +240,6 @@ void EpubReaderActivity::loop() { } } -void EpubReaderActivity::onReaderMenuBack(const uint8_t orientation) { - exitActivity(); - // Apply the user-selected orientation when the menu is dismissed. - // This ensures the menu can be navigated without immediately rotating the screen. - applyOrientation(orientation); - requestUpdate(); -} - // 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) { @@ -348,82 +306,44 @@ void EpubReaderActivity::jumpToPercent(int percent) { void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) { switch (action) { case EpubReaderMenuActivity::MenuAction::SELECT_CHAPTER: { - // Calculate values BEFORE we start destroying things - const int currentP = section ? section->currentPage : 0; - const int totalP = section ? section->pageCount : 0; const int spineIdx = currentSpineIndex; const std::string path = epub->getPath(); - - // 1. Close the menu - exitActivity(); - - // 2. Open the Chapter Selector - enterNewActivity(new EpubReaderChapterSelectionActivity( - this->renderer, this->mappedInput, epub, path, spineIdx, currentP, totalP, - [this] { - exitActivity(); - requestUpdate(); - }, - [this](const int newSpineIndex) { - if (currentSpineIndex != newSpineIndex) { - currentSpineIndex = newSpineIndex; + startActivityForResult( + std::make_unique(renderer, mappedInput, epub, path, spineIdx), + [this](const ActivityResult& result) { + if (!result.isCancelled && currentSpineIndex != std::get(result.data).spineIndex) { + currentSpineIndex = std::get(result.data).spineIndex; nextPageNumber = 0; section.reset(); } - exitActivity(); - requestUpdate(); - }, - [this](const int newSpineIndex, const int newPage) { - if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) { - currentSpineIndex = newSpineIndex; - nextPageNumber = newPage; - section.reset(); - } - exitActivity(); - requestUpdate(); - })); - + }); break; } case EpubReaderMenuActivity::MenuAction::FOOTNOTES: { - exitActivity(); - enterNewActivity(new EpubReaderFootnotesActivity( - this->renderer, this->mappedInput, currentPageFootnotes, - [this] { - // Go back from footnotes list - exitActivity(); - requestUpdate(); - }, - [this](const char* href) { - // Navigate to selected footnote - navigateToHref(href, true); - exitActivity(); - requestUpdate(); - })); + startActivityForResult(std::make_unique(renderer, mappedInput, currentPageFootnotes), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& footnoteResult = std::get(result.data); + navigateToHref(footnoteResult.href, true); + } + requestUpdate(); + }); break; } case EpubReaderMenuActivity::MenuAction::GO_TO_PERCENT: { - // Launch the slider-based percent selector and return here on confirm/cancel. float bookProgress = 0.0f; if (epub && epub->getBookSize() > 0 && section && section->pageCount > 0) { const float chapterProgress = static_cast(section->currentPage) / static_cast(section->pageCount); bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; } const int initialPercent = clampPercent(static_cast(bookProgress + 0.5f)); - exitActivity(); - enterNewActivity(new EpubReaderPercentSelectionActivity( - renderer, mappedInput, initialPercent, - [this](const int percent) { - // Apply the new position and exit back to the reader. - jumpToPercent(percent); - exitActivity(); - requestUpdate(); - }, - [this]() { - // Cancel selection and return to the reader. - exitActivity(); - requestUpdate(); - })); + startActivityForResult( + std::make_unique(renderer, mappedInput, initialPercent), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + jumpToPercent(std::get(result.data).percent); + } + }); break; } case EpubReaderMenuActivity::MenuAction::DISPLAY_QR: { @@ -444,55 +364,41 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction } } if (!fullText.empty()) { - exitActivity(); - enterNewActivity(new QrDisplayActivity(renderer, mappedInput, fullText, [this]() { - exitActivity(); - requestUpdate(); - })); + startActivityForResult(std::make_unique(renderer, mappedInput, fullText), + [this](const ActivityResult& result) {}); break; } } } // If no text or page loading failed, just close menu - exitActivity(); requestUpdate(); break; } case EpubReaderMenuActivity::MenuAction::GO_HOME: { - // Defer go home to avoid race condition with display task - pendingGoHome = true; - break; + onGoHome(); + return; } case EpubReaderMenuActivity::MenuAction::DELETE_CACHE: { { RenderLock lock(*this); - if (epub) { - // 2. BACKUP: Read current progress - // We use the current variables that track our position + if (epub && section) { uint16_t backupSpine = currentSpineIndex; uint16_t backupPage = section->currentPage; uint16_t backupPageCount = section->pageCount; - section.reset(); - // 3. WIPE: Clear the cache directory epub->clearCache(); - - // 4. RESTORE: Re-setup the directory and rewrite the progress file epub->setupCacheDir(); - saveProgress(backupSpine, backupPage, backupPageCount); } } - // Defer go home to avoid race condition with display task - pendingGoHome = true; - break; + onGoHome(); + return; } case EpubReaderMenuActivity::MenuAction::SCREENSHOT: { { RenderLock lock(*this); pendingScreenshot = true; } - exitActivity(); requestUpdate(); break; } @@ -500,22 +406,19 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction if (KOREADER_STORE.hasCredentials()) { const int currentPage = section ? section->currentPage : 0; const int totalPages = section ? section->pageCount : 0; - exitActivity(); - enterNewActivity(new KOReaderSyncActivity( - renderer, mappedInput, epub, epub->getPath(), currentSpineIndex, currentPage, totalPages, - [this]() { - // On cancel - defer exit to avoid use-after-free - pendingSubactivityExit = true; - }, - [this](int newSpineIndex, int newPage) { - // On sync complete - update position and defer exit - if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) { - currentSpineIndex = newSpineIndex; - nextPageNumber = newPage; - section.reset(); + startActivityForResult( + std::make_unique(renderer, mappedInput, epub, epub->getPath(), currentSpineIndex, + currentPage, totalPages), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& sync = std::get(result.data); + if (currentSpineIndex != sync.spineIndex || (section && section->currentPage != sync.page)) { + currentSpineIndex = sync.spineIndex; + nextPageNumber = sync.page; + section.reset(); + } } - pendingSubactivityExit = true; - })); + }); } break; } @@ -550,7 +453,7 @@ void EpubReaderActivity::applyOrientation(const uint8_t orientation) { } // TODO: Failure handling -void EpubReaderActivity::render(Activity::RenderLock&& lock) { +void EpubReaderActivity::render(RenderLock&& lock) { if (!epub) { return; } @@ -785,8 +688,8 @@ void EpubReaderActivity::renderStatusBar() const { GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title); } -void EpubReaderActivity::navigateToHref(const char* href, const bool savePosition) { - if (!epub || !href) return; +void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) { + if (!epub) return; // Push current position onto saved stack if (savePosition && section && footnoteDepth < MAX_FOOTNOTE_DEPTH) { @@ -795,8 +698,6 @@ void EpubReaderActivity::navigateToHref(const char* href, const bool savePositio LOG_DBG("ERS", "Saved position [%d]: spine %d, page %d", footnoteDepth, currentSpineIndex, section->currentPage); } - std::string hrefStr(href); - // Check for same-file anchor reference (#anchor only) bool sameFile = !hrefStr.empty() && hrefStr[0] == '#'; @@ -809,7 +710,7 @@ void EpubReaderActivity::navigateToHref(const char* href, const bool savePositio } if (targetSpineIndex < 0) { - LOG_DBG("ERS", "Could not resolve href: %s", href); + LOG_DBG("ERS", "Could not resolve href: %s", hrefStr.c_str()); if (savePosition && footnoteDepth > 0) footnoteDepth--; // undo push return; } @@ -821,7 +722,7 @@ void EpubReaderActivity::navigateToHref(const char* href, const bool savePositio section.reset(); } requestUpdate(); - LOG_DBG("ERS", "Navigated to spine %d for href: %s", targetSpineIndex, href); + LOG_DBG("ERS", "Navigated to spine %d for href: %s", targetSpineIndex, hrefStr.c_str()); } void EpubReaderActivity::restoreSavedPosition() { diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index cc5ae465..0d1c4567 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -4,9 +4,9 @@ #include #include "EpubReaderMenuActivity.h" -#include "activities/ActivityWithSubactivity.h" +#include "activities/Activity.h" -class EpubReaderActivity final : public ActivityWithSubactivity { +class EpubReaderActivity final : public Activity { std::shared_ptr epub; std::unique_ptr
section = nullptr; int currentSpineIndex = 0; @@ -19,12 +19,8 @@ class EpubReaderActivity final : public ActivityWithSubactivity { bool pendingPercentJump = false; // Normalized 0.0-1.0 progress within the target spine item, computed from book percentage. float pendingSpineProgress = 0.0f; - bool pendingSubactivityExit = false; // Defer subactivity exit to avoid use-after-free - bool pendingGoHome = false; // Defer go home to avoid race condition with display task bool pendingScreenshot = false; bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit - const std::function onGoBack; - const std::function onGoHome; // Footnote support std::vector currentPageFootnotes; @@ -42,23 +38,19 @@ class EpubReaderActivity final : public ActivityWithSubactivity { 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); - void onReaderMenuBack(uint8_t orientation); void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action); void applyOrientation(uint8_t orientation); // Footnote navigation - void navigateToHref(const char* href, bool savePosition = false); + void navigateToHref(const std::string& href, bool savePosition = false); void restoreSavedPosition(); public: - explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr epub, - const std::function& onGoBack, const std::function& onGoHome) - : ActivityWithSubactivity("EpubReader", renderer, mappedInput), - epub(std::move(epub)), - onGoBack(onGoBack), - onGoHome(onGoHome) {} + explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr epub) + : Activity("EpubReader", renderer, mappedInput), epub(std::move(epub)) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&& lock) override; + void render(RenderLock&& lock) override; + bool isReaderActivity() const override { return true; } }; diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index 6089a7d2..ea68bd8f 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -26,7 +26,7 @@ int EpubReaderChapterSelectionActivity::getPageItems() const { } void EpubReaderChapterSelectionActivity::onEnter() { - ActivityWithSubactivity::onEnter(); + Activity::onEnter(); if (!epub) { return; @@ -41,26 +41,28 @@ void EpubReaderChapterSelectionActivity::onEnter() { requestUpdate(); } -void EpubReaderChapterSelectionActivity::onExit() { ActivityWithSubactivity::onExit(); } +void EpubReaderChapterSelectionActivity::onExit() { Activity::onExit(); } void EpubReaderChapterSelectionActivity::loop() { - if (subActivity) { - subActivity->loop(); - return; - } - const int pageItems = getPageItems(); const int totalItems = getTotalItems(); if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { const auto newSpineIndex = epub->getSpineIndexForTocIndex(selectorIndex); if (newSpineIndex == -1) { - onGoBack(); + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); } else { - onSelectSpineIndex(newSpineIndex); + setResult(ChapterResult{newSpineIndex}); + finish(); } } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - onGoBack(); + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); } buttonNavigator.onNextRelease([this, totalItems] { @@ -84,7 +86,7 @@ void EpubReaderChapterSelectionActivity::loop() { }); } -void EpubReaderChapterSelectionActivity::render(Activity::RenderLock&&) { +void EpubReaderChapterSelectionActivity::render(RenderLock&&) { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.h b/src/activities/reader/EpubReaderChapterSelectionActivity.h index 28a6f165..20b53aa4 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.h +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.h @@ -3,22 +3,16 @@ #include -#include "../ActivityWithSubactivity.h" +#include "../Activity.h" #include "util/ButtonNavigator.h" -class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity { +class EpubReaderChapterSelectionActivity final : public Activity { std::shared_ptr epub; std::string epubPath; ButtonNavigator buttonNavigator; int currentSpineIndex = 0; - int currentPage = 0; - int totalPagesInSpine = 0; int selectorIndex = 0; - const std::function onGoBack; - const std::function onSelectSpineIndex; - const std::function onSyncPosition; - // Number of items that fit on a page, derived from logical screen height. // This adapts automatically when switching between portrait and landscape. int getPageItems() const; @@ -29,21 +23,13 @@ class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity public: explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::shared_ptr& epub, const std::string& epubPath, - const int currentSpineIndex, const int currentPage, - const int totalPagesInSpine, const std::function& onGoBack, - const std::function& onSelectSpineIndex, - const std::function& onSyncPosition) - : ActivityWithSubactivity("EpubReaderChapterSelection", renderer, mappedInput), + const int currentSpineIndex) + : Activity("EpubReaderChapterSelection", renderer, mappedInput), epub(epub), epubPath(epubPath), - currentSpineIndex(currentSpineIndex), - currentPage(currentPage), - totalPagesInSpine(totalPagesInSpine), - onGoBack(onGoBack), - onSelectSpineIndex(onSelectSpineIndex), - onSyncPosition(onSyncPosition) {} + currentSpineIndex(currentSpineIndex) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; }; diff --git a/src/activities/reader/EpubReaderFootnotesActivity.cpp b/src/activities/reader/EpubReaderFootnotesActivity.cpp index 8558de81..ef077a07 100644 --- a/src/activities/reader/EpubReaderFootnotesActivity.cpp +++ b/src/activities/reader/EpubReaderFootnotesActivity.cpp @@ -10,27 +10,26 @@ #include "fontIds.h" void EpubReaderFootnotesActivity::onEnter() { - ActivityWithSubactivity::onEnter(); + Activity::onEnter(); selectedIndex = 0; requestUpdate(); } -void EpubReaderFootnotesActivity::onExit() { ActivityWithSubactivity::onExit(); } +void EpubReaderFootnotesActivity::onExit() { Activity::onExit(); } void EpubReaderFootnotesActivity::loop() { - if (subActivity) { - subActivity->loop(); - return; - } - if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - onGoBack(); + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); return; } if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (selectedIndex >= 0 && selectedIndex < static_cast(footnotes.size())) { - onSelectFootnote(footnotes[selectedIndex].href); + setResult(FootnoteResult{footnotes[selectedIndex].href}); + finish(); } return; } @@ -50,7 +49,7 @@ void EpubReaderFootnotesActivity::loop() { }); } -void EpubReaderFootnotesActivity::render(Activity::RenderLock&&) { +void EpubReaderFootnotesActivity::render(RenderLock&&) { renderer.clearScreen(); renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_FOOTNOTES), true, EpdFontFamily::BOLD); diff --git a/src/activities/reader/EpubReaderFootnotesActivity.h b/src/activities/reader/EpubReaderFootnotesActivity.h index ad11909d..7336d038 100644 --- a/src/activities/reader/EpubReaderFootnotesActivity.h +++ b/src/activities/reader/EpubReaderFootnotesActivity.h @@ -6,29 +6,22 @@ #include #include -#include "../ActivityWithSubactivity.h" +#include "../Activity.h" #include "util/ButtonNavigator.h" -class EpubReaderFootnotesActivity final : public ActivityWithSubactivity { +class EpubReaderFootnotesActivity final : public Activity { public: explicit EpubReaderFootnotesActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::vector& footnotes, - const std::function& onGoBack, - const std::function& onSelectFootnote) - : ActivityWithSubactivity("EpubReaderFootnotes", renderer, mappedInput), - footnotes(footnotes), - onGoBack(onGoBack), - onSelectFootnote(onSelectFootnote) {} + const std::vector& footnotes) + : Activity("EpubReaderFootnotes", renderer, mappedInput), footnotes(footnotes) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; private: const std::vector& footnotes; - const std::function onGoBack; - const std::function onSelectFootnote; int selectedIndex = 0; int scrollOffset = 0; ButtonNavigator buttonNavigator; diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index b6121516..5afb77e4 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -10,17 +10,14 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, const int currentPage, const int totalPages, const int bookProgressPercent, const uint8_t currentOrientation, - const bool hasFootnotes, const std::function& onBack, - const std::function& onAction) - : ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput), + const bool hasFootnotes) + : Activity("EpubReaderMenu", renderer, mappedInput), menuItems(buildMenuItems(hasFootnotes)), title(title), pendingOrientation(currentOrientation), currentPage(currentPage), totalPages(totalPages), - bookProgressPercent(bookProgressPercent), - onBack(onBack), - onAction(onAction) {} + bookProgressPercent(bookProgressPercent) {} std::vector EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) { std::vector items; @@ -40,18 +37,13 @@ std::vector EpubReaderMenuActivity::buildMenuI } void EpubReaderMenuActivity::onEnter() { - ActivityWithSubactivity::onEnter(); + Activity::onEnter(); requestUpdate(); } -void EpubReaderMenuActivity::onExit() { ActivityWithSubactivity::onExit(); } +void EpubReaderMenuActivity::onExit() { Activity::onExit(); } void EpubReaderMenuActivity::loop() { - if (subActivity) { - subActivity->loop(); - return; - } - // Handle navigation buttonNavigator.onNext([this] { selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast(menuItems.size())); @@ -63,7 +55,6 @@ void EpubReaderMenuActivity::loop() { requestUpdate(); }); - // Use local variables for items we need to check after potential deletion if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { const auto selectedAction = menuItems[selectedIndex].action; if (selectedAction == MenuAction::ROTATE_SCREEN) { @@ -73,22 +64,20 @@ void EpubReaderMenuActivity::loop() { return; } - // 1. Capture the callback and action locally - auto actionCallback = onAction; - - // 2. Execute the callback - actionCallback(selectedAction); - - // 3. CRITICAL: Return immediately. 'this' is likely deleted now. + setResult(MenuResult{static_cast(selectedAction), pendingOrientation}); + finish(); return; } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - // Return the pending orientation to the parent so it can apply on exit. - onBack(pendingOrientation); - return; // Also return here just in case + ActivityResult result; + result.isCancelled = true; + result.data = MenuResult{-1, pendingOrientation}; + setResult(std::move(result)); + finish(); + return; } } -void EpubReaderMenuActivity::render(Activity::RenderLock&&) { +void EpubReaderMenuActivity::render(RenderLock&&) { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); const auto orientation = renderer.getOrientation(); diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 037d342c..4748c2f1 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -2,14 +2,13 @@ #include #include -#include #include #include -#include "../ActivityWithSubactivity.h" +#include "../Activity.h" #include "util/ButtonNavigator.h" -class EpubReaderMenuActivity final : public ActivityWithSubactivity { +class EpubReaderMenuActivity final : public Activity { public: // Menu actions available from the reader menu. enum class MenuAction { @@ -26,14 +25,12 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, const int currentPage, const int totalPages, const int bookProgressPercent, - const uint8_t currentOrientation, const bool hasFootnotes, - const std::function& onBack, - const std::function& onAction); + const uint8_t currentOrientation, const bool hasFootnotes); void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; private: struct MenuItem { @@ -56,7 +53,4 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { int currentPage = 0; int totalPages = 0; int bookProgressPercent = 0; - - const std::function onBack; - const std::function onAction; }; diff --git a/src/activities/reader/EpubReaderPercentSelectionActivity.cpp b/src/activities/reader/EpubReaderPercentSelectionActivity.cpp index 188d19e2..db4109f0 100644 --- a/src/activities/reader/EpubReaderPercentSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderPercentSelectionActivity.cpp @@ -14,12 +14,12 @@ constexpr int kLargeStep = 10; } // namespace void EpubReaderPercentSelectionActivity::onEnter() { - ActivityWithSubactivity::onEnter(); + Activity::onEnter(); // Set up rendering task and mark first frame dirty. requestUpdate(); } -void EpubReaderPercentSelectionActivity::onExit() { ActivityWithSubactivity::onExit(); } +void EpubReaderPercentSelectionActivity::onExit() { Activity::onExit(); } void EpubReaderPercentSelectionActivity::adjustPercent(const int delta) { // Apply delta and clamp within 0-100. @@ -33,19 +33,18 @@ void EpubReaderPercentSelectionActivity::adjustPercent(const int delta) { } void EpubReaderPercentSelectionActivity::loop() { - if (subActivity) { - subActivity->loop(); - return; - } - // Back cancels, confirm selects, arrows adjust the percent. if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - onCancel(); + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); return; } if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - onSelect(percent); + setResult(PercentResult{percent}); + finish(); return; } @@ -56,7 +55,7 @@ void EpubReaderPercentSelectionActivity::loop() { buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { adjustPercent(-kLargeStep); }); } -void EpubReaderPercentSelectionActivity::render(Activity::RenderLock&&) { +void EpubReaderPercentSelectionActivity::render(RenderLock&&) { renderer.clearScreen(); // Title and numeric percent value. diff --git a/src/activities/reader/EpubReaderPercentSelectionActivity.h b/src/activities/reader/EpubReaderPercentSelectionActivity.h index 08502d88..8cba8664 100644 --- a/src/activities/reader/EpubReaderPercentSelectionActivity.h +++ b/src/activities/reader/EpubReaderPercentSelectionActivity.h @@ -1,26 +1,20 @@ #pragma once -#include - #include "MappedInputManager.h" -#include "activities/ActivityWithSubactivity.h" +#include "activities/Activity.h" #include "util/ButtonNavigator.h" -class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity { +class EpubReaderPercentSelectionActivity final : public Activity { public: // Slider-style percent selector for jumping within a book. explicit EpubReaderPercentSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const int initialPercent, const std::function& onSelect, - const std::function& onCancel) - : ActivityWithSubactivity("EpubReaderPercentSelection", renderer, mappedInput), - percent(initialPercent), - onSelect(onSelect), - onCancel(onCancel) {} + const int initialPercent) + : Activity("EpubReaderPercentSelection", renderer, mappedInput), percent(initialPercent) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; private: // Current percent value (0-100) shown on the slider. @@ -28,11 +22,6 @@ class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity ButtonNavigator buttonNavigator; - // Callback invoked when the user confirms a percent. - const std::function onSelect; - // Callback invoked when the user cancels the slider. - const std::function onCancel; - // Change the current percent by a delta and clamp within bounds. void adjustPercent(int delta); }; diff --git a/src/activities/reader/KOReaderSyncActivity.cpp b/src/activities/reader/KOReaderSyncActivity.cpp index 7f9f5d66..251bd716 100644 --- a/src/activities/reader/KOReaderSyncActivity.cpp +++ b/src/activities/reader/KOReaderSyncActivity.cpp @@ -51,11 +51,12 @@ void wifiOff() { } // namespace void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { - exitActivity(); - if (!success) { LOG_DBG("KOSync", "WiFi connection failed, exiting"); - onCancel(); + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); return; } @@ -66,7 +67,7 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { state = SYNCING; statusMessage = tr(STR_SYNCING_TIME); } - requestUpdate(); + requestUpdate(true); // Sync time with NTP before making API requests syncTimeWithNTP(); @@ -75,7 +76,7 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { RenderLock lock(*this); statusMessage = tr(STR_CALC_HASH); } - requestUpdate(); + requestUpdate(true); performSync(); } @@ -93,7 +94,7 @@ void KOReaderSyncActivity::performSync() { state = SYNC_FAILED; statusMessage = tr(STR_HASH_FAILED); } - requestUpdate(); + requestUpdate(true); return; } @@ -115,7 +116,7 @@ void KOReaderSyncActivity::performSync() { state = NO_REMOTE_PROGRESS; hasRemoteProgress = false; } - requestUpdate(); + requestUpdate(true); return; } @@ -125,7 +126,7 @@ void KOReaderSyncActivity::performSync() { state = SYNC_FAILED; statusMessage = KOReaderSyncClient::errorString(result); } - requestUpdate(); + requestUpdate(true); return; } @@ -149,7 +150,7 @@ void KOReaderSyncActivity::performSync() { selectedOption = 0; // Apply remote progress } } - requestUpdate(); + requestUpdate(true); } void KOReaderSyncActivity::performUpload() { @@ -158,7 +159,6 @@ void KOReaderSyncActivity::performUpload() { state = UPLOADING; statusMessage = tr(STR_UPLOAD_PROGRESS); } - requestUpdate(); requestUpdateAndWait(); // Convert current position to KOReader format @@ -188,11 +188,11 @@ void KOReaderSyncActivity::performUpload() { RenderLock lock(*this); state = UPLOAD_COMPLETE; } - requestUpdate(); + requestUpdate(true); } void KOReaderSyncActivity::onEnter() { - ActivityWithSubactivity::onEnter(); + Activity::onEnter(); // Check for credentials first if (!KOREADER_STORE.hasCredentials()) { @@ -206,7 +206,7 @@ void KOReaderSyncActivity::onEnter() { LOG_DBG("KOSync", "Already connected to WiFi"); state = SYNCING; statusMessage = tr(STR_SYNCING_TIME); - requestUpdate(); + requestUpdate(true); // Perform sync directly (will be handled in loop) xTaskCreate( @@ -218,7 +218,7 @@ void KOReaderSyncActivity::onEnter() { RenderLock lock(*self); self->statusMessage = tr(STR_CALC_HASH); } - self->requestUpdate(); + self->requestUpdate(true); self->performSync(); vTaskDelete(nullptr); }, @@ -228,21 +228,17 @@ void KOReaderSyncActivity::onEnter() { // Launch WiFi selection subactivity LOG_DBG("KOSync", "Launching WifiSelectionActivity..."); - enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, - [this](const bool connected) { onWifiSelectionComplete(connected); })); + startActivityForResult(std::make_unique(renderer, mappedInput), + [this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); }); } void KOReaderSyncActivity::onExit() { - ActivityWithSubactivity::onExit(); + Activity::onExit(); wifiOff(); } -void KOReaderSyncActivity::render(Activity::RenderLock&&) { - if (subActivity) { - return; - } - +void KOReaderSyncActivity::render(RenderLock&&) { const auto pageWidth = renderer.getScreenWidth(); renderer.clearScreen(); @@ -357,49 +353,50 @@ void KOReaderSyncActivity::render(Activity::RenderLock&&) { } void KOReaderSyncActivity::loop() { - if (subActivity) { - subActivity->loop(); - return; - } - if (state == NO_CREDENTIALS || state == SYNC_FAILED || state == UPLOAD_COMPLETE) { - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { - onCancel(); + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); } return; } if (state == SHOWING_RESULT) { // Navigate options - if (mappedInput.wasPressed(MappedInputManager::Button::Up) || - mappedInput.wasPressed(MappedInputManager::Button::Left)) { + if (mappedInput.wasReleased(MappedInputManager::Button::Up) || + mappedInput.wasReleased(MappedInputManager::Button::Left)) { selectedOption = (selectedOption + 1) % 2; // Wrap around among 2 options requestUpdate(); - } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || - mappedInput.wasPressed(MappedInputManager::Button::Right)) { + } else if (mappedInput.wasReleased(MappedInputManager::Button::Down) || + mappedInput.wasReleased(MappedInputManager::Button::Right)) { selectedOption = (selectedOption + 1) % 2; // Wrap around among 2 options requestUpdate(); } - if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (selectedOption == 0) { - // Apply remote progress — WiFi no longer needed - wifiOff(); - onSyncComplete(remotePosition.spineIndex, remotePosition.pageNumber); + // Wifi will be turned off in onExit() + setResult(SyncResult{remotePosition.spineIndex, remotePosition.pageNumber}); + finish(); } else if (selectedOption == 1) { // Upload local progress performUpload(); } } - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { - onCancel(); + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); } return; } if (state == NO_REMOTE_PROGRESS) { - if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { // Calculate hash if not done yet if (documentHash.empty()) { if (KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME) { @@ -411,8 +408,11 @@ void KOReaderSyncActivity::loop() { performUpload(); } - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { - onCancel(); + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); } return; } diff --git a/src/activities/reader/KOReaderSyncActivity.h b/src/activities/reader/KOReaderSyncActivity.h index bd29bc0a..71bdbf2f 100644 --- a/src/activities/reader/KOReaderSyncActivity.h +++ b/src/activities/reader/KOReaderSyncActivity.h @@ -6,7 +6,7 @@ #include "KOReaderSyncClient.h" #include "ProgressMapper.h" -#include "activities/ActivityWithSubactivity.h" +#include "activities/Activity.h" /** * Activity for syncing reading progress with KOReader sync server. @@ -18,16 +18,12 @@ * 4. Show comparison and options (Apply/Upload) * 5. Apply or upload progress */ -class KOReaderSyncActivity final : public ActivityWithSubactivity { +class KOReaderSyncActivity final : public Activity { public: - using OnCancelCallback = std::function; - using OnSyncCompleteCallback = std::function; - explicit KOReaderSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::shared_ptr& epub, const std::string& epubPath, int currentSpineIndex, - int currentPage, int totalPagesInSpine, OnCancelCallback onCancel, - OnSyncCompleteCallback onSyncComplete) - : ActivityWithSubactivity("KOReaderSync", renderer, mappedInput), + int currentPage, int totalPagesInSpine) + : Activity("KOReaderSync", renderer, mappedInput), epub(epub), epubPath(epubPath), currentSpineIndex(currentSpineIndex), @@ -35,14 +31,12 @@ class KOReaderSyncActivity final : public ActivityWithSubactivity { totalPagesInSpine(totalPagesInSpine), remoteProgress{}, remotePosition{}, - localProgress{}, - onCancel(std::move(onCancel)), - onSyncComplete(std::move(onSyncComplete)) {} + localProgress{} {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; bool preventAutoSleep() override { return state == CONNECTING || state == SYNCING; } private: @@ -79,9 +73,6 @@ class KOReaderSyncActivity final : public ActivityWithSubactivity { // Selection in result screen (0=Apply, 1=Upload) int selectedOption = 0; - OnCancelCallback onCancel; - OnSyncCompleteCallback onSyncComplete; - void onWifiSelectionComplete(bool success); void performSync(); void performUpload(); diff --git a/src/activities/reader/QrDisplayActivity.cpp b/src/activities/reader/QrDisplayActivity.cpp index 95a04b52..2bcf330a 100644 --- a/src/activities/reader/QrDisplayActivity.cpp +++ b/src/activities/reader/QrDisplayActivity.cpp @@ -18,12 +18,12 @@ void QrDisplayActivity::onExit() { Activity::onExit(); } void QrDisplayActivity::loop() { if (mappedInput.wasReleased(MappedInputManager::Button::Back) || mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - onGoBack(); + finish(); return; } } -void QrDisplayActivity::render(Activity::RenderLock&&) { +void QrDisplayActivity::render(RenderLock&&) { renderer.clearScreen(); auto metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); diff --git a/src/activities/reader/QrDisplayActivity.h b/src/activities/reader/QrDisplayActivity.h index f3958af7..3cfdb6b3 100644 --- a/src/activities/reader/QrDisplayActivity.h +++ b/src/activities/reader/QrDisplayActivity.h @@ -7,16 +7,14 @@ class QrDisplayActivity final : public Activity { public: - explicit QrDisplayActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& textPayload, - const std::function& onGoBack) - : Activity("QrDisplay", renderer, mappedInput), textPayload(textPayload), onGoBack(onGoBack) {} + explicit QrDisplayActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& textPayload) + : Activity("QrDisplay", renderer, mappedInput), textPayload(textPayload) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; private: std::string textPayload; - const std::function onGoBack; }; diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index 9c191f15..82a1e23f 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -79,41 +79,34 @@ std::unique_ptr ReaderActivity::loadTxt(const std::string& path) { void ReaderActivity::goToLibrary(const std::string& fromBookPath) { // If coming from a book, start in that book's folder; otherwise start from root - const auto initialPath = fromBookPath.empty() ? "/" : extractFolderPath(fromBookPath); - onGoToLibrary(initialPath); + auto initialPath = fromBookPath.empty() ? "/" : extractFolderPath(fromBookPath); + activityManager.goToMyLibrary(std::move(initialPath)); } void ReaderActivity::onGoToEpubReader(std::unique_ptr epub) { const auto epubPath = epub->getPath(); currentBookPath = epubPath; - exitActivity(); - enterNewActivity(new EpubReaderActivity( - renderer, mappedInput, std::move(epub), [this, epubPath] { goToLibrary(epubPath); }, [this] { onGoBack(); })); + activityManager.replaceActivity(std::make_unique(renderer, mappedInput, std::move(epub))); } void ReaderActivity::onGoToBmpViewer(const std::string& path) { - exitActivity(); - enterNewActivity(new BmpViewerActivity(renderer, mappedInput, path, [this, path] { goToLibrary(path); })); + activityManager.replaceActivity(std::make_unique(renderer, mappedInput, path)); } void ReaderActivity::onGoToXtcReader(std::unique_ptr xtc) { const auto xtcPath = xtc->getPath(); currentBookPath = xtcPath; - exitActivity(); - enterNewActivity(new XtcReaderActivity( - renderer, mappedInput, std::move(xtc), [this, xtcPath] { goToLibrary(xtcPath); }, [this] { onGoBack(); })); + activityManager.replaceActivity(std::make_unique(renderer, mappedInput, std::move(xtc))); } void ReaderActivity::onGoToTxtReader(std::unique_ptr txt) { const auto txtPath = txt->getPath(); currentBookPath = txtPath; - exitActivity(); - enterNewActivity(new TxtReaderActivity( - renderer, mappedInput, std::move(txt), [this, txtPath] { goToLibrary(txtPath); }, [this] { onGoBack(); })); + activityManager.replaceActivity(std::make_unique(renderer, mappedInput, std::move(txt))); } void ReaderActivity::onEnter() { - ActivityWithSubactivity::onEnter(); + Activity::onEnter(); if (initialBookPath.empty()) { goToLibrary(); // Start from root when entering via Browse @@ -146,3 +139,5 @@ void ReaderActivity::onEnter() { onGoToEpubReader(std::move(epub)); } } + +void ReaderActivity::onGoBack() { finish(); } diff --git a/src/activities/reader/ReaderActivity.h b/src/activities/reader/ReaderActivity.h index 52a3e1d1..601a35f7 100644 --- a/src/activities/reader/ReaderActivity.h +++ b/src/activities/reader/ReaderActivity.h @@ -1,18 +1,16 @@ #pragma once #include -#include "../ActivityWithSubactivity.h" +#include "../Activity.h" #include "activities/home/MyLibraryActivity.h" class Epub; class Xtc; class Txt; -class ReaderActivity final : public ActivityWithSubactivity { +class ReaderActivity final : public Activity { std::string initialBookPath; std::string currentBookPath; // Track current book path for navigation - const std::function onGoBack; - const std::function onGoToLibrary; static std::unique_ptr loadEpub(const std::string& path); static std::unique_ptr loadXtc(const std::string& path); static std::unique_ptr loadTxt(const std::string& path); @@ -27,14 +25,11 @@ class ReaderActivity final : public ActivityWithSubactivity { void onGoToTxtReader(std::unique_ptr txt); void onGoToBmpViewer(const std::string& path); + void onGoBack(); + public: - explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath, - const std::function& onGoBack, - const std::function& onGoToLibrary) - : ActivityWithSubactivity("Reader", renderer, mappedInput), - initialBookPath(std::move(initialBookPath)), - onGoBack(onGoBack), - onGoToLibrary(onGoToLibrary) {} + explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath) + : Activity("Reader", renderer, mappedInput), initialBookPath(std::move(initialBookPath)) {} void onEnter() override; bool isReaderActivity() const override { return true; } }; diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 6535069c..b41f2ba1 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -23,7 +23,7 @@ constexpr uint8_t CACHE_VERSION = 2; // Increment when cache format cha } // namespace void TxtReaderActivity::onEnter() { - ActivityWithSubactivity::onEnter(); + Activity::onEnter(); if (!txt) { return; @@ -61,7 +61,7 @@ void TxtReaderActivity::onEnter() { } void TxtReaderActivity::onExit() { - ActivityWithSubactivity::onExit(); + Activity::onExit(); // Reset orientation back to portrait for the rest of the UI renderer.setOrientation(GfxRenderer::Orientation::Portrait); @@ -74,14 +74,9 @@ void TxtReaderActivity::onExit() { } void TxtReaderActivity::loop() { - if (subActivity) { - subActivity->loop(); - return; - } - // Long press BACK (1s+) goes to file selection if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) { - onGoBack(); + activityManager.goToMyLibrary(txt ? txt->getPath() : ""); return; } @@ -325,7 +320,7 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector return !outLines.empty(); } -void TxtReaderActivity::render(Activity::RenderLock&&) { +void TxtReaderActivity::render(RenderLock&&) { if (!txt) { return; } diff --git a/src/activities/reader/TxtReaderActivity.h b/src/activities/reader/TxtReaderActivity.h index c3b2ad7f..45877a8e 100644 --- a/src/activities/reader/TxtReaderActivity.h +++ b/src/activities/reader/TxtReaderActivity.h @@ -5,18 +5,15 @@ #include #include "CrossPointSettings.h" -#include "activities/ActivityWithSubactivity.h" +#include "activities/Activity.h" -class TxtReaderActivity final : public ActivityWithSubactivity { +class TxtReaderActivity final : public Activity { std::unique_ptr txt; int currentPage = 0; int totalPages = 1; int pagesUntilFullRefresh = 0; - const std::function onGoBack; - const std::function onGoHome; - // Streaming text reader - stores file offsets for each page std::vector pageOffsets; // File offset for start of each page std::vector currentPageLines; @@ -45,14 +42,11 @@ class TxtReaderActivity final : public ActivityWithSubactivity { void loadProgress(); public: - explicit TxtReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr txt, - const std::function& onGoBack, const std::function& onGoHome) - : ActivityWithSubactivity("TxtReader", renderer, mappedInput), - txt(std::move(txt)), - onGoBack(onGoBack), - onGoHome(onGoHome) {} + explicit TxtReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr txt) + : Activity("TxtReader", renderer, mappedInput), txt(std::move(txt)) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; + bool isReaderActivity() const override { return true; } }; diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 8e10fb67..a10d1025 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -26,7 +26,7 @@ constexpr unsigned long goHomeMs = 1000; } // namespace void XtcReaderActivity::onEnter() { - ActivityWithSubactivity::onEnter(); + Activity::onEnter(); if (!xtc) { return; @@ -47,7 +47,7 @@ void XtcReaderActivity::onEnter() { } void XtcReaderActivity::onExit() { - ActivityWithSubactivity::onExit(); + Activity::onExit(); APP_STATE.readerActivityLoadCount = 0; APP_STATE.saveToFile(); @@ -55,33 +55,22 @@ void XtcReaderActivity::onExit() { } void XtcReaderActivity::loop() { - // Pass input responsibility to sub activity if exists - if (subActivity) { - subActivity->loop(); - return; - } - // Enter chapter selection activity if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) { - exitActivity(); - enterNewActivity(new XtcReaderChapterSelectionActivity( - this->renderer, this->mappedInput, xtc, currentPage, - [this] { - exitActivity(); - requestUpdate(); - }, - [this](const uint32_t newPage) { - currentPage = newPage; - exitActivity(); - requestUpdate(); - })); + startActivityForResult( + std::make_unique(renderer, mappedInput, xtc, currentPage), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + currentPage = std::get(result.data).page; + } + }); } } // Long press BACK (1s+) goes to file selection if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) { - onGoBack(); + activityManager.goToMyLibrary(xtc ? xtc->getPath() : ""); return; } @@ -135,7 +124,7 @@ void XtcReaderActivity::loop() { } } -void XtcReaderActivity::render(Activity::RenderLock&&) { +void XtcReaderActivity::render(RenderLock&&) { if (!xtc) { return; } diff --git a/src/activities/reader/XtcReaderActivity.h b/src/activities/reader/XtcReaderActivity.h index c9e8997c..18effaad 100644 --- a/src/activities/reader/XtcReaderActivity.h +++ b/src/activities/reader/XtcReaderActivity.h @@ -9,30 +9,24 @@ #include -#include "activities/ActivityWithSubactivity.h" +#include "activities/Activity.h" -class XtcReaderActivity final : public ActivityWithSubactivity { +class XtcReaderActivity final : public Activity { std::shared_ptr xtc; uint32_t currentPage = 0; int pagesUntilFullRefresh = 0; - const std::function onGoBack; - const std::function onGoHome; - void renderPage(); void saveProgress() const; void loadProgress(); public: - explicit XtcReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr xtc, - const std::function& onGoBack, const std::function& onGoHome) - : ActivityWithSubactivity("XtcReader", renderer, mappedInput), - xtc(std::move(xtc)), - onGoBack(onGoBack), - onGoHome(onGoHome) {} + explicit XtcReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr xtc) + : Activity("XtcReader", renderer, mappedInput), xtc(std::move(xtc)) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; + bool isReaderActivity() const override { return true; } }; diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp index 8518a495..99d6b28d 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp @@ -59,10 +59,14 @@ void XtcReaderChapterSelectionActivity::loop() { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { const auto& chapters = xtc->getChapters(); if (!chapters.empty() && selectorIndex >= 0 && selectorIndex < static_cast(chapters.size())) { - onSelectPage(chapters[selectorIndex].startPage); + setResult(PageResult{chapters[selectorIndex].startPage}); + finish(); } } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - onGoBack(); + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); } buttonNavigator.onNextRelease([this, totalItems] { @@ -86,7 +90,7 @@ void XtcReaderChapterSelectionActivity::loop() { }); } -void XtcReaderChapterSelectionActivity::render(Activity::RenderLock&&) { +void XtcReaderChapterSelectionActivity::render(RenderLock&&) { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.h b/src/activities/reader/XtcReaderChapterSelectionActivity.h index b45217a1..75d44215 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.h +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.h @@ -12,24 +12,16 @@ class XtcReaderChapterSelectionActivity final : public Activity { uint32_t currentPage = 0; int selectorIndex = 0; - const std::function onGoBack; - const std::function onSelectPage; - int getPageItems() const; int findChapterIndexForPage(uint32_t page) const; public: explicit XtcReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::shared_ptr& xtc, uint32_t currentPage, - const std::function& onGoBack, - const std::function& onSelectPage) - : Activity("XtcReaderChapterSelection", renderer, mappedInput), - xtc(xtc), - currentPage(currentPage), - onGoBack(onGoBack), - onSelectPage(onSelectPage) {} + const std::shared_ptr& xtc, uint32_t currentPage) + : Activity("XtcReaderChapterSelection", renderer, mappedInput), xtc(xtc), currentPage(currentPage) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; + bool isReaderActivity() const override { return true; } }; diff --git a/src/activities/settings/ButtonRemapActivity.cpp b/src/activities/settings/ButtonRemapActivity.cpp index 92c3140d..8e2e8d7a 100644 --- a/src/activities/settings/ButtonRemapActivity.cpp +++ b/src/activities/settings/ButtonRemapActivity.cpp @@ -52,20 +52,20 @@ void ButtonRemapActivity::loop() { SETTINGS.frontButtonLeft = CrossPointSettings::FRONT_HW_LEFT; SETTINGS.frontButtonRight = CrossPointSettings::FRONT_HW_RIGHT; SETTINGS.saveToFile(); - onBack(); + finish(); return; } if (mappedInput.wasPressed(MappedInputManager::Button::Down)) { // Exit without changing settings. - onBack(); + finish(); return; } { - // Wait for the UI to refresh before accepting another assignment. + // Make sure UI done rendering before accepting another assignment. // This avoids rapid double-presses that can advance the step without a visible redraw. - requestUpdateAndWait(); + RenderLock lock(*this); // Wait for a front button press to assign to the current role. const int pressedButton = mappedInput.getPressedFrontButton(); @@ -86,7 +86,7 @@ void ButtonRemapActivity::loop() { // All roles assigned; save to settings and exit. applyTempMapping(); SETTINGS.saveToFile(); - onBack(); + finish(); return; } @@ -94,7 +94,7 @@ void ButtonRemapActivity::loop() { } } -void ButtonRemapActivity::render(Activity::RenderLock&&) { +void ButtonRemapActivity::render(RenderLock&&) { const auto labelForHardware = [&](uint8_t hardwareIndex) -> const char* { for (uint8_t i = 0; i < kRoleCount; i++) { if (tempMapping[i] == hardwareIndex) { diff --git a/src/activities/settings/ButtonRemapActivity.h b/src/activities/settings/ButtonRemapActivity.h index 5d5ba1a9..0fb130b1 100644 --- a/src/activities/settings/ButtonRemapActivity.h +++ b/src/activities/settings/ButtonRemapActivity.h @@ -7,20 +7,17 @@ class ButtonRemapActivity final : public Activity { public: - explicit ButtonRemapActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onBack) - : Activity("ButtonRemap", renderer, mappedInput), onBack(onBack) {} + explicit ButtonRemapActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("ButtonRemap", renderer, mappedInput) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; private: // Rendering task state. - // Callback used to exit the remap flow back to the settings list. - const std::function onBack; // Index of the logical role currently awaiting input. uint8_t currentStep = 0; // Temporary mapping from logical role -> hardware button index. diff --git a/src/activities/settings/CalibreSettingsActivity.cpp b/src/activities/settings/CalibreSettingsActivity.cpp index 421a0b7e..7cd6ac1b 100644 --- a/src/activities/settings/CalibreSettingsActivity.cpp +++ b/src/activities/settings/CalibreSettingsActivity.cpp @@ -17,22 +17,17 @@ const StrId menuNames[MENU_ITEMS] = {StrId::STR_CALIBRE_WEB_URL, StrId::STR_USER } // namespace void CalibreSettingsActivity::onEnter() { - ActivityWithSubactivity::onEnter(); + Activity::onEnter(); selectedIndex = 0; requestUpdate(); } -void CalibreSettingsActivity::onExit() { ActivityWithSubactivity::onExit(); } +void CalibreSettingsActivity::onExit() { Activity::onExit(); } void CalibreSettingsActivity::loop() { - if (subActivity) { - subActivity->loop(); - return; - } - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { - onBack(); + activityManager.popActivity(); return; } @@ -56,62 +51,44 @@ void CalibreSettingsActivity::loop() { void CalibreSettingsActivity::handleSelection() { if (selectedIndex == 0) { // OPDS Server URL - exitActivity(); - enterNewActivity(new KeyboardEntryActivity( - renderer, mappedInput, tr(STR_CALIBRE_WEB_URL), SETTINGS.opdsServerUrl, - 127, // maxLength - false, // not password - [this](const std::string& url) { - strncpy(SETTINGS.opdsServerUrl, url.c_str(), sizeof(SETTINGS.opdsServerUrl) - 1); - SETTINGS.opdsServerUrl[sizeof(SETTINGS.opdsServerUrl) - 1] = '\0'; - SETTINGS.saveToFile(); - exitActivity(); - requestUpdate(); - }, - [this]() { - exitActivity(); - requestUpdate(); - })); + startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_CALIBRE_WEB_URL), + SETTINGS.opdsServerUrl, 127, false), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& kb = std::get(result.data); + strncpy(SETTINGS.opdsServerUrl, kb.text.c_str(), sizeof(SETTINGS.opdsServerUrl) - 1); + SETTINGS.opdsServerUrl[sizeof(SETTINGS.opdsServerUrl) - 1] = '\0'; + SETTINGS.saveToFile(); + } + }); } else if (selectedIndex == 1) { // Username - exitActivity(); - enterNewActivity(new KeyboardEntryActivity( - renderer, mappedInput, tr(STR_USERNAME), SETTINGS.opdsUsername, - 63, // maxLength - false, // not password - [this](const std::string& username) { - strncpy(SETTINGS.opdsUsername, username.c_str(), sizeof(SETTINGS.opdsUsername) - 1); - SETTINGS.opdsUsername[sizeof(SETTINGS.opdsUsername) - 1] = '\0'; - SETTINGS.saveToFile(); - exitActivity(); - requestUpdate(); - }, - [this]() { - exitActivity(); - requestUpdate(); - })); + startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_USERNAME), + SETTINGS.opdsUsername, 63, false), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& kb = std::get(result.data); + strncpy(SETTINGS.opdsUsername, kb.text.c_str(), sizeof(SETTINGS.opdsUsername) - 1); + SETTINGS.opdsUsername[sizeof(SETTINGS.opdsUsername) - 1] = '\0'; + SETTINGS.saveToFile(); + } + }); } else if (selectedIndex == 2) { // Password - exitActivity(); - enterNewActivity(new KeyboardEntryActivity( - renderer, mappedInput, tr(STR_PASSWORD), SETTINGS.opdsPassword, - 63, // maxLength - false, // not password mode - [this](const std::string& password) { - strncpy(SETTINGS.opdsPassword, password.c_str(), sizeof(SETTINGS.opdsPassword) - 1); - SETTINGS.opdsPassword[sizeof(SETTINGS.opdsPassword) - 1] = '\0'; - SETTINGS.saveToFile(); - exitActivity(); - requestUpdate(); - }, - [this]() { - exitActivity(); - requestUpdate(); - })); + startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_PASSWORD), + SETTINGS.opdsPassword, 63, false), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& kb = std::get(result.data); + strncpy(SETTINGS.opdsPassword, kb.text.c_str(), sizeof(SETTINGS.opdsPassword) - 1); + SETTINGS.opdsPassword[sizeof(SETTINGS.opdsPassword) - 1] = '\0'; + SETTINGS.saveToFile(); + } + }); } } -void CalibreSettingsActivity::render(Activity::RenderLock&&) { +void CalibreSettingsActivity::render(RenderLock&&) { renderer.clearScreen(); const auto& metrics = UITheme::getInstance().getMetrics(); diff --git a/src/activities/settings/CalibreSettingsActivity.h b/src/activities/settings/CalibreSettingsActivity.h index 7f5d4dcd..c7990d9a 100644 --- a/src/activities/settings/CalibreSettingsActivity.h +++ b/src/activities/settings/CalibreSettingsActivity.h @@ -1,29 +1,25 @@ #pragma once -#include - -#include "activities/ActivityWithSubactivity.h" +#include "activities/Activity.h" #include "util/ButtonNavigator.h" /** * Submenu for OPDS Browser settings. * Shows OPDS Server URL and HTTP authentication options. */ -class CalibreSettingsActivity final : public ActivityWithSubactivity { +class CalibreSettingsActivity final : public Activity { public: - explicit CalibreSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onBack) - : ActivityWithSubactivity("CalibreSettings", renderer, mappedInput), onBack(onBack) {} + explicit CalibreSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("CalibreSettings", renderer, mappedInput) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; private: ButtonNavigator buttonNavigator; size_t selectedIndex = 0; - const std::function onBack; void handleSelection(); }; diff --git a/src/activities/settings/ClearCacheActivity.cpp b/src/activities/settings/ClearCacheActivity.cpp index ca3a9a6a..c4fc4347 100644 --- a/src/activities/settings/ClearCacheActivity.cpp +++ b/src/activities/settings/ClearCacheActivity.cpp @@ -10,15 +10,15 @@ #include "fontIds.h" void ClearCacheActivity::onEnter() { - ActivityWithSubactivity::onEnter(); + Activity::onEnter(); state = WARNING; requestUpdate(); } -void ClearCacheActivity::onExit() { ActivityWithSubactivity::onExit(); } +void ClearCacheActivity::onExit() { Activity::onExit(); } -void ClearCacheActivity::render(Activity::RenderLock&&) { +void ClearCacheActivity::render(RenderLock&&) { const auto& metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); diff --git a/src/activities/settings/ClearCacheActivity.h b/src/activities/settings/ClearCacheActivity.h index 5976e212..712f7a9c 100644 --- a/src/activities/settings/ClearCacheActivity.h +++ b/src/activities/settings/ClearCacheActivity.h @@ -2,26 +2,25 @@ #include -#include "activities/ActivityWithSubactivity.h" +#include "activities/Activity.h" -class ClearCacheActivity final : public ActivityWithSubactivity { +class ClearCacheActivity final : public Activity { public: - explicit ClearCacheActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& goBack) - : ActivityWithSubactivity("ClearCache", renderer, mappedInput), goBack(goBack) {} + explicit ClearCacheActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("ClearCache", renderer, mappedInput) {} void onEnter() override; void onExit() override; void loop() override; bool skipLoopDelay() override { return true; } // Prevent power-saving mode - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; private: enum State { WARNING, CLEARING, SUCCESS, FAILED }; State state = WARNING; - const std::function goBack; + void goBack() { finish(); } int clearedCount = 0; int failedCount = 0; diff --git a/src/activities/settings/KOReaderAuthActivity.cpp b/src/activities/settings/KOReaderAuthActivity.cpp index c6367b35..138369bc 100644 --- a/src/activities/settings/KOReaderAuthActivity.cpp +++ b/src/activities/settings/KOReaderAuthActivity.cpp @@ -12,8 +12,6 @@ #include "fontIds.h" void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) { - exitActivity(); - if (!success) { { RenderLock lock(*this); @@ -51,7 +49,7 @@ void KOReaderAuthActivity::performAuthentication() { } void KOReaderAuthActivity::onEnter() { - ActivityWithSubactivity::onEnter(); + Activity::onEnter(); // Turn on WiFi WiFi.mode(WIFI_STA); @@ -74,12 +72,12 @@ void KOReaderAuthActivity::onEnter() { } // Launch WiFi selection - enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, - [this](const bool connected) { onWifiSelectionComplete(connected); })); + startActivityForResult(std::make_unique(renderer, mappedInput), + [this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); }); } void KOReaderAuthActivity::onExit() { - ActivityWithSubactivity::onExit(); + Activity::onExit(); // Turn off wifi WiFi.disconnect(false); @@ -88,7 +86,7 @@ void KOReaderAuthActivity::onExit() { delay(100); } -void KOReaderAuthActivity::render(Activity::RenderLock&&) { +void KOReaderAuthActivity::render(RenderLock&&) { renderer.clearScreen(); const auto& metrics = UITheme::getInstance().getMetrics(); @@ -115,15 +113,10 @@ void KOReaderAuthActivity::render(Activity::RenderLock&&) { } void KOReaderAuthActivity::loop() { - if (subActivity) { - subActivity->loop(); - return; - } - if (state == SUCCESS || state == FAILED) { if (mappedInput.wasPressed(MappedInputManager::Button::Back) || mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { - onComplete(); + finish(); } } } diff --git a/src/activities/settings/KOReaderAuthActivity.h b/src/activities/settings/KOReaderAuthActivity.h index 4b1bbe6d..b4e330e7 100644 --- a/src/activities/settings/KOReaderAuthActivity.h +++ b/src/activities/settings/KOReaderAuthActivity.h @@ -2,22 +2,21 @@ #include -#include "activities/ActivityWithSubactivity.h" +#include "activities/Activity.h" /** * Activity for testing KOReader credentials. * Connects to WiFi and authenticates with the KOReader sync server. */ -class KOReaderAuthActivity final : public ActivityWithSubactivity { +class KOReaderAuthActivity final : public Activity { public: - explicit KOReaderAuthActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onComplete) - : ActivityWithSubactivity("KOReaderAuth", renderer, mappedInput), onComplete(onComplete) {} + explicit KOReaderAuthActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("KOReaderAuth", renderer, mappedInput) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; bool preventAutoSleep() override { return state == CONNECTING || state == AUTHENTICATING; } private: @@ -27,8 +26,6 @@ class KOReaderAuthActivity final : public ActivityWithSubactivity { std::string statusMessage; std::string errorMessage; - const std::function onComplete; - void onWifiSelectionComplete(bool success); void performAuthentication(); }; diff --git a/src/activities/settings/KOReaderSettingsActivity.cpp b/src/activities/settings/KOReaderSettingsActivity.cpp index e629a775..b84076f2 100644 --- a/src/activities/settings/KOReaderSettingsActivity.cpp +++ b/src/activities/settings/KOReaderSettingsActivity.cpp @@ -19,22 +19,17 @@ const StrId menuNames[MENU_ITEMS] = {StrId::STR_USERNAME, StrId::STR_PASSWORD, S } // namespace void KOReaderSettingsActivity::onEnter() { - ActivityWithSubactivity::onEnter(); + Activity::onEnter(); selectedIndex = 0; requestUpdate(); } -void KOReaderSettingsActivity::onExit() { ActivityWithSubactivity::onExit(); } +void KOReaderSettingsActivity::onExit() { Activity::onExit(); } void KOReaderSettingsActivity::loop() { - if (subActivity) { - subActivity->loop(); - return; - } - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { - onBack(); + activityManager.popActivity(); return; } @@ -58,59 +53,46 @@ void KOReaderSettingsActivity::loop() { void KOReaderSettingsActivity::handleSelection() { if (selectedIndex == 0) { // Username - exitActivity(); - enterNewActivity(new KeyboardEntryActivity( - renderer, mappedInput, tr(STR_KOREADER_USERNAME), KOREADER_STORE.getUsername(), - 64, // maxLength - false, // not password - [this](const std::string& username) { - KOREADER_STORE.setCredentials(username, KOREADER_STORE.getPassword()); - KOREADER_STORE.saveToFile(); - exitActivity(); - requestUpdate(); - }, - [this]() { - exitActivity(); - requestUpdate(); - })); + startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_KOREADER_USERNAME), + KOREADER_STORE.getUsername(), + 64, // maxLength + false), // not password + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& kb = std::get(result.data); + KOREADER_STORE.setCredentials(kb.text, KOREADER_STORE.getPassword()); + KOREADER_STORE.saveToFile(); + } + }); } else if (selectedIndex == 1) { // Password - exitActivity(); - enterNewActivity(new KeyboardEntryActivity( - renderer, mappedInput, tr(STR_KOREADER_PASSWORD), KOREADER_STORE.getPassword(), - 64, // maxLength - false, // show characters - [this](const std::string& password) { - KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), password); - KOREADER_STORE.saveToFile(); - exitActivity(); - requestUpdate(); - }, - [this]() { - exitActivity(); - requestUpdate(); - })); + startActivityForResult(std::make_unique(renderer, mappedInput, tr(STR_KOREADER_PASSWORD), + KOREADER_STORE.getPassword(), + 64, // maxLength + false), // show characters + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& kb = std::get(result.data); + KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), kb.text); + KOREADER_STORE.saveToFile(); + } + }); } else if (selectedIndex == 2) { // Sync Server URL - prefill with https:// if empty to save typing const std::string currentUrl = KOREADER_STORE.getServerUrl(); const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl; - exitActivity(); - enterNewActivity(new KeyboardEntryActivity( - renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, - 128, // maxLength - URLs can be long - false, // not password - [this](const std::string& url) { - // Clear if user just left the prefilled https:// - const std::string urlToSave = (url == "https://" || url == "http://") ? "" : url; - KOREADER_STORE.setServerUrl(urlToSave); - KOREADER_STORE.saveToFile(); - exitActivity(); - requestUpdate(); - }, - [this]() { - exitActivity(); - requestUpdate(); - })); + startActivityForResult( + std::make_unique(renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, + 128, // maxLength - URLs can be long + false), // not password + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& kb = std::get(result.data); + const std::string urlToSave = (kb.text == "https://" || kb.text == "http://") ? "" : kb.text; + KOREADER_STORE.setServerUrl(urlToSave); + KOREADER_STORE.saveToFile(); + } + }); } else if (selectedIndex == 3) { // Document Matching - toggle between Filename and Binary const auto current = KOREADER_STORE.getMatchMethod(); @@ -125,15 +107,11 @@ void KOReaderSettingsActivity::handleSelection() { // Can't authenticate without credentials - just show message briefly return; } - exitActivity(); - enterNewActivity(new KOReaderAuthActivity(renderer, mappedInput, [this] { - exitActivity(); - requestUpdate(); - })); + startActivityForResult(std::make_unique(renderer, mappedInput), [](const ActivityResult&) {}); } } -void KOReaderSettingsActivity::render(Activity::RenderLock&&) { +void KOReaderSettingsActivity::render(RenderLock&&) { renderer.clearScreen(); const auto& metrics = UITheme::getInstance().getMetrics(); diff --git a/src/activities/settings/KOReaderSettingsActivity.h b/src/activities/settings/KOReaderSettingsActivity.h index a4af49c9..f32db8a5 100644 --- a/src/activities/settings/KOReaderSettingsActivity.h +++ b/src/activities/settings/KOReaderSettingsActivity.h @@ -1,30 +1,26 @@ #pragma once -#include - -#include "activities/ActivityWithSubactivity.h" +#include "activities/Activity.h" #include "util/ButtonNavigator.h" /** * Submenu for KOReader Sync settings. * Shows username, password, and authenticate options. */ -class KOReaderSettingsActivity final : public ActivityWithSubactivity { +class KOReaderSettingsActivity final : public Activity { public: - explicit KOReaderSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onBack) - : ActivityWithSubactivity("KOReaderSettings", renderer, mappedInput), onBack(onBack) {} + explicit KOReaderSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("KOReaderSettings", renderer, mappedInput) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; private: ButtonNavigator buttonNavigator; size_t selectedIndex = 0; - const std::function onBack; void handleSelection(); }; diff --git a/src/activities/settings/LanguageSelectActivity.cpp b/src/activities/settings/LanguageSelectActivity.cpp index 349a021f..e2d41755 100644 --- a/src/activities/settings/LanguageSelectActivity.cpp +++ b/src/activities/settings/LanguageSelectActivity.cpp @@ -58,7 +58,7 @@ void LanguageSelectActivity::handleSelection() { onBack(); } -void LanguageSelectActivity::render(Activity::RenderLock&&) { +void LanguageSelectActivity::render(RenderLock&&) { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); diff --git a/src/activities/settings/LanguageSelectActivity.h b/src/activities/settings/LanguageSelectActivity.h index 6af007de..b8662554 100644 --- a/src/activities/settings/LanguageSelectActivity.h +++ b/src/activities/settings/LanguageSelectActivity.h @@ -5,7 +5,7 @@ #include -#include "../ActivityWithSubactivity.h" +#include "../Activity.h" #include "components/UITheme.h" #include "util/ButtonNavigator.h" @@ -16,19 +16,18 @@ class MappedInputManager; */ class LanguageSelectActivity final : public Activity { public: - explicit LanguageSelectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onBack) - : Activity("LanguageSelect", renderer, mappedInput), onBack(onBack) {} + explicit LanguageSelectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("LanguageSelect", renderer, mappedInput) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; private: void handleSelection(); - std::function onBack; + void onBack() { finish(); } ButtonNavigator buttonNavigator; int selectedIndex = 0; constexpr static uint8_t totalItems = getLanguageCount(); diff --git a/src/activities/settings/OtaUpdateActivity.cpp b/src/activities/settings/OtaUpdateActivity.cpp index beb142b7..8009adb3 100644 --- a/src/activities/settings/OtaUpdateActivity.cpp +++ b/src/activities/settings/OtaUpdateActivity.cpp @@ -11,11 +11,9 @@ #include "network/OtaUpdater.h" void OtaUpdateActivity::onWifiSelectionComplete(const bool success) { - exitActivity(); - if (!success) { LOG_ERR("OTA", "WiFi connection failed, exiting"); - goBack(); + activityManager.popActivity(); return; } @@ -34,7 +32,6 @@ void OtaUpdateActivity::onWifiSelectionComplete(const bool success) { RenderLock lock(*this); state = FAILED; } - requestUpdate(); return; } @@ -44,7 +41,6 @@ void OtaUpdateActivity::onWifiSelectionComplete(const bool success) { RenderLock lock(*this); state = NO_UPDATE; } - requestUpdate(); return; } @@ -52,11 +48,10 @@ void OtaUpdateActivity::onWifiSelectionComplete(const bool success) { RenderLock lock(*this); state = WAITING_CONFIRMATION; } - requestUpdate(); } void OtaUpdateActivity::onEnter() { - ActivityWithSubactivity::onEnter(); + Activity::onEnter(); // Turn on WiFi immediately LOG_DBG("OTA", "Turning on WiFi..."); @@ -64,12 +59,12 @@ void OtaUpdateActivity::onEnter() { // Launch WiFi selection subactivity LOG_DBG("OTA", "Launching WifiSelectionActivity..."); - enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, - [this](const bool connected) { onWifiSelectionComplete(connected); })); + startActivityForResult(std::make_unique(renderer, mappedInput), + [this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); }); } void OtaUpdateActivity::onExit() { - ActivityWithSubactivity::onExit(); + Activity::onExit(); // Turn off wifi WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame @@ -78,12 +73,7 @@ void OtaUpdateActivity::onExit() { delay(100); // Allow WiFi hardware to fully power down } -void OtaUpdateActivity::render(Activity::RenderLock&&) { - if (subActivity) { - // Subactivity handles its own rendering - return; - } - +void OtaUpdateActivity::render(RenderLock&&) { const auto& metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); @@ -154,11 +144,6 @@ void OtaUpdateActivity::loop() { requestUpdate(); } - if (subActivity) { - subActivity->loop(); - return; - } - if (state == WAITING_CONFIRMATION) { if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { LOG_DBG("OTA", "New update available, starting download..."); @@ -166,7 +151,6 @@ void OtaUpdateActivity::loop() { RenderLock lock(*this); state = UPDATE_IN_PROGRESS; } - requestUpdate(); requestUpdateAndWait(); const auto res = updater.installUpdate(); @@ -188,7 +172,7 @@ void OtaUpdateActivity::loop() { } if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { - goBack(); + activityManager.popActivity(); } return; @@ -196,14 +180,14 @@ void OtaUpdateActivity::loop() { if (state == FAILED) { if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { - goBack(); + activityManager.popActivity(); } return; } if (state == NO_UPDATE) { if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { - goBack(); + activityManager.popActivity(); } return; } diff --git a/src/activities/settings/OtaUpdateActivity.h b/src/activities/settings/OtaUpdateActivity.h index 76711c1b..12afc80d 100644 --- a/src/activities/settings/OtaUpdateActivity.h +++ b/src/activities/settings/OtaUpdateActivity.h @@ -1,9 +1,9 @@ #pragma once -#include "activities/ActivityWithSubactivity.h" +#include "activities/Activity.h" #include "network/OtaUpdater.h" -class OtaUpdateActivity : public ActivityWithSubactivity { +class OtaUpdateActivity : public Activity { enum State { WIFI_SELECTION, CHECKING_FOR_UPDATE, @@ -18,7 +18,6 @@ class OtaUpdateActivity : public ActivityWithSubactivity { // Can't initialize this to 0 or the first render doesn't happen static constexpr unsigned int UNINITIALIZED_PERCENTAGE = 111; - const std::function goBack; State state = WIFI_SELECTION; unsigned int lastUpdaterPercentage = UNINITIALIZED_PERCENTAGE; OtaUpdater updater; @@ -26,13 +25,12 @@ class OtaUpdateActivity : public ActivityWithSubactivity { void onWifiSelectionComplete(bool success); public: - explicit OtaUpdateActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& goBack) - : ActivityWithSubactivity("OtaUpdate", renderer, mappedInput), goBack(goBack), updater() {} + explicit OtaUpdateActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("OtaUpdate", renderer, mappedInput), updater() {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; bool preventAutoSleep() override { return state == CHECKING_FOR_UPDATE || state == UPDATE_IN_PROGRESS; } bool skipLoopDelay() override { return true; } // Prevent power-saving mode }; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index eab84cbb..5912a2d8 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -67,16 +67,12 @@ void SettingsActivity::onEnter() { } void SettingsActivity::onExit() { - ActivityWithSubactivity::onExit(); + Activity::onExit(); UITheme::getInstance().reload(); // Re-apply theme in case it was changed } void SettingsActivity::loop() { - if (subActivity) { - subActivity->loop(); - return; - } bool hasChangedCategory = false; // Handle actions with early return @@ -164,50 +160,38 @@ void SettingsActivity::toggleCurrentSetting() { SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step; } } else if (setting.type == SettingType::ACTION) { - auto enterSubActivity = [this](Activity* activity) { - exitActivity(); - enterNewActivity(activity); - }; - - auto onComplete = [this] { - exitActivity(); - requestUpdate(); - }; - - auto onCompleteBool = [this](bool) { - exitActivity(); - requestUpdate(); - }; + auto resultHandler = [this](const ActivityResult&) { SETTINGS.saveToFile(); }; switch (setting.action) { case SettingAction::RemapFrontButtons: - enterSubActivity(new ButtonRemapActivity(renderer, mappedInput, onComplete)); + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); break; case SettingAction::CustomiseStatusBar: - enterSubActivity(new StatusBarSettingsActivity(renderer, mappedInput, onComplete)); + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); break; case SettingAction::KOReaderSync: - enterSubActivity(new KOReaderSettingsActivity(renderer, mappedInput, onComplete)); + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); break; case SettingAction::OPDSBrowser: - enterSubActivity(new CalibreSettingsActivity(renderer, mappedInput, onComplete)); + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); break; case SettingAction::Network: - enterSubActivity(new WifiSelectionActivity(renderer, mappedInput, onCompleteBool, false)); + startActivityForResult(std::make_unique(renderer, mappedInput, false), resultHandler); break; case SettingAction::ClearCache: - enterSubActivity(new ClearCacheActivity(renderer, mappedInput, onComplete)); + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); break; case SettingAction::CheckForUpdates: - enterSubActivity(new OtaUpdateActivity(renderer, mappedInput, onComplete)); + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); break; case SettingAction::Language: - enterSubActivity(new LanguageSelectActivity(renderer, mappedInput, onComplete)); + startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); break; case SettingAction::None: // Do nothing break; } + return; // Results will be handled in the result handler, so we can return early here } else { return; } @@ -215,7 +199,7 @@ void SettingsActivity::toggleCurrentSetting() { SETTINGS.saveToFile(); } -void SettingsActivity::render(Activity::RenderLock&&) { +void SettingsActivity::render(RenderLock&&) { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 62f98dc5..75fefa43 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -5,7 +5,7 @@ #include #include -#include "activities/ActivityWithSubactivity.h" +#include "activities/Activity.h" #include "util/ButtonNavigator.h" class CrossPointSettings; @@ -134,7 +134,7 @@ struct SettingInfo { } }; -class SettingsActivity final : public ActivityWithSubactivity { +class SettingsActivity final : public Activity { ButtonNavigator buttonNavigator; int selectedCategoryIndex = 0; // Currently selected category @@ -148,8 +148,6 @@ class SettingsActivity final : public ActivityWithSubactivity { std::vector systemSettings; const std::vector* currentSettings = nullptr; - const std::function onGoHome; - static constexpr int categoryCount = 4; static const StrId categoryNames[categoryCount]; @@ -157,11 +155,10 @@ class SettingsActivity final : public ActivityWithSubactivity { void toggleCurrentSetting(); public: - explicit SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onGoHome) - : ActivityWithSubactivity("Settings", renderer, mappedInput), onGoHome(onGoHome) {} + explicit SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("Settings", renderer, mappedInput) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; }; diff --git a/src/activities/settings/StatusBarSettingsActivity.cpp b/src/activities/settings/StatusBarSettingsActivity.cpp index f216b941..e347e7a1 100644 --- a/src/activities/settings/StatusBarSettingsActivity.cpp +++ b/src/activities/settings/StatusBarSettingsActivity.cpp @@ -58,7 +58,7 @@ void StatusBarSettingsActivity::onExit() { Activity::onExit(); } void StatusBarSettingsActivity::loop() { if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { - onBack(); + finish(); return; } @@ -114,7 +114,7 @@ void StatusBarSettingsActivity::handleSelection() { SETTINGS.saveToFile(); } -void StatusBarSettingsActivity::render(Activity::RenderLock&&) { +void StatusBarSettingsActivity::render(RenderLock&&) { renderer.clearScreen(); auto metrics = UITheme::getInstance().getMetrics(); diff --git a/src/activities/settings/StatusBarSettingsActivity.h b/src/activities/settings/StatusBarSettingsActivity.h index 4c7e4ed0..2cbc86dc 100644 --- a/src/activities/settings/StatusBarSettingsActivity.h +++ b/src/activities/settings/StatusBarSettingsActivity.h @@ -9,22 +9,18 @@ // Reader status bar configuration activity class StatusBarSettingsActivity final : public Activity { public: - explicit StatusBarSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onBack) - : Activity("StatusBarSettings", renderer, mappedInput), onBack(onBack) {} + explicit StatusBarSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) + : Activity("StatusBarSettings", renderer, mappedInput) {} void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; private: ButtonNavigator buttonNavigator; int selectedIndex = 0; - const std::function onBack; - - static void taskTrampoline(void* param); void handleSelection(); }; diff --git a/src/activities/util/BmpViewerActivity.cpp b/src/activities/util/BmpViewerActivity.cpp index 46dec3c7..51fd12b1 100644 --- a/src/activities/util/BmpViewerActivity.cpp +++ b/src/activities/util/BmpViewerActivity.cpp @@ -8,9 +8,8 @@ #include "components/UITheme.h" #include "fontIds.h" -BmpViewerActivity::BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string path, - std::function onGoBack) - : Activity("BmpViewer", renderer, mappedInput), filePath(std::move(path)), onGoBack(std::move(onGoBack)) {} +BmpViewerActivity::BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string path) + : Activity("BmpViewer", renderer, mappedInput), filePath(std::move(path)) {} void BmpViewerActivity::onEnter() { Activity::onEnter(); @@ -95,7 +94,7 @@ void BmpViewerActivity::loop() { Activity::loop(); if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - if (onGoBack) onGoBack(); + onGoHome(); return; } } \ No newline at end of file diff --git a/src/activities/util/BmpViewerActivity.h b/src/activities/util/BmpViewerActivity.h index c3fba379..feac448e 100644 --- a/src/activities/util/BmpViewerActivity.h +++ b/src/activities/util/BmpViewerActivity.h @@ -8,8 +8,7 @@ class BmpViewerActivity final : public Activity { public: - BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string filePath, - std::function onGoBack); + BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string filePath); void onEnter() override; void onExit() override; @@ -17,5 +16,4 @@ class BmpViewerActivity final : public Activity { private: std::string filePath; - std::function onGoBack; }; \ No newline at end of file diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index 41e6e56a..b763a2b6 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -57,13 +57,13 @@ char KeyboardEntryActivity::getSelectedChar() const { return layout[selectedRow][selectedCol]; } -void KeyboardEntryActivity::handleKeyPress() { +bool KeyboardEntryActivity::handleKeyPress() { // Handle special row (bottom row with shift, space, backspace, done) if (selectedRow == SPECIAL_ROW) { if (selectedCol >= SHIFT_COL && selectedCol < SPACE_COL) { // Shift toggle (0 = lower case, 1 = upper case, 2 = shift lock) shiftState = (shiftState + 1) % 3; - return; + return true; } if (selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL) { @@ -71,7 +71,7 @@ void KeyboardEntryActivity::handleKeyPress() { if (maxLength == 0 || text.length() < maxLength) { text += ' '; } - return; + return true; } if (selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL) { @@ -79,22 +79,20 @@ void KeyboardEntryActivity::handleKeyPress() { if (!text.empty()) { text.pop_back(); } - return; + return true; } if (selectedCol >= DONE_COL) { // Done button - if (onComplete) { - onComplete(text); - } - return; + onComplete(text); + return false; } } // Regular character const char c = getSelectedChar(); if (c == '\0') { - return; + return true; } if (maxLength == 0 || text.length() < maxLength) { @@ -104,6 +102,8 @@ void KeyboardEntryActivity::handleKeyPress() { shiftState = 0; } } + + return true; } void KeyboardEntryActivity::loop() { @@ -177,20 +177,19 @@ void KeyboardEntryActivity::loop() { // Selection if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { - handleKeyPress(); - requestUpdate(); + if (handleKeyPress()) { + requestUpdate(); + } + // If handleKeyPress returns false, it means onComplete was triggered, no update needed } // Cancel if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { - if (onCancel) { - onCancel(); - } - requestUpdate(); + onCancel(); } } -void KeyboardEntryActivity::render(Activity::RenderLock&&) { +void KeyboardEntryActivity::render(RenderLock&&) { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); @@ -321,3 +320,15 @@ void KeyboardEntryActivity::render(Activity::RenderLock&&) { renderer.displayBuffer(); } + +void KeyboardEntryActivity::onComplete(std::string text) { + setResult(KeyboardResult{std::move(text)}); + finish(); +} + +void KeyboardEntryActivity::onCancel() { + ActivityResult result; + result.isCancelled = true; + setResult(std::move(result)); + finish(); +} diff --git a/src/activities/util/KeyboardEntryActivity.h b/src/activities/util/KeyboardEntryActivity.h index c498b256..e7e6b8cf 100644 --- a/src/activities/util/KeyboardEntryActivity.h +++ b/src/activities/util/KeyboardEntryActivity.h @@ -10,21 +10,10 @@ /** * Reusable keyboard entry activity for text input. - * Can be started from any activity that needs text entry. - * - * Usage: - * 1. Create a KeyboardEntryActivity instance - * 2. Set callbacks with setOnComplete() and setOnCancel() - * 3. Call onEnter() to start the activity - * 4. Call loop() in your main loop - * 5. When complete or cancelled, callbacks will be invoked + * Can be started from any activity that needs text entry via startActivityForResult() */ class KeyboardEntryActivity : public Activity { public: - // Callback types - using OnCompleteCallback = std::function; - using OnCancelCallback = std::function; - /** * Constructor * @param renderer Reference to the GfxRenderer for drawing @@ -33,26 +22,21 @@ class KeyboardEntryActivity : public Activity { * @param initialText Initial text to show in the input field * @param maxLength Maximum length of input text (0 for unlimited) * @param isPassword If true, display asterisks instead of actual characters - * @param onComplete Callback invoked when input is complete - * @param onCancel Callback invoked when input is cancelled */ explicit KeyboardEntryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string title = "Enter Text", std::string initialText = "", - const size_t maxLength = 0, const bool isPassword = false, - OnCompleteCallback onComplete = nullptr, OnCancelCallback onCancel = nullptr) + const size_t maxLength = 0, const bool isPassword = false) : Activity("KeyboardEntry", renderer, mappedInput), title(std::move(title)), text(std::move(initialText)), maxLength(maxLength), - isPassword(isPassword), - onComplete(std::move(onComplete)), - onCancel(std::move(onCancel)) {} + isPassword(isPassword) {} // Activity overrides void onEnter() override; void onExit() override; void loop() override; - void render(Activity::RenderLock&&) override; + void render(RenderLock&&) override; private: std::string title; @@ -67,9 +51,9 @@ class KeyboardEntryActivity : public Activity { int selectedCol = 0; int shiftState = 0; // 0 = lower case, 1 = upper case, 2 = shift lock) - // Callbacks - OnCompleteCallback onComplete; - OnCancelCallback onCancel; + // Handlers + void onComplete(std::string text); + void onCancel(); // Keyboard layout static constexpr int NUM_ROWS = 5; @@ -86,6 +70,6 @@ class KeyboardEntryActivity : public Activity { static constexpr int DONE_COL = 9; char getSelectedChar() const; - void handleKeyPress(); + bool handleKeyPress(); // false if onComplete was triggered int getRowLength(int row) const; }; diff --git a/src/main.cpp b/src/main.cpp index 097f0e22..b27eb52c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -18,16 +18,8 @@ #include "KOReaderCredentialStore.h" #include "MappedInputManager.h" #include "RecentBooksStore.h" -#include "activities/boot_sleep/BootActivity.h" -#include "activities/boot_sleep/SleepActivity.h" -#include "activities/browser/OpdsBookBrowserActivity.h" -#include "activities/home/HomeActivity.h" -#include "activities/home/MyLibraryActivity.h" -#include "activities/home/RecentBooksActivity.h" -#include "activities/network/CrossPointWebServerActivity.h" -#include "activities/reader/ReaderActivity.h" -#include "activities/settings/SettingsActivity.h" -#include "activities/util/FullScreenMessageActivity.h" +#include "activities/Activity.h" +#include "activities/ActivityManager.h" #include "components/UITheme.h" #include "fontIds.h" #include "util/ButtonNavigator.h" @@ -37,8 +29,8 @@ HalDisplay display; HalGPIO gpio; MappedInputManager mappedInputManager(gpio); GfxRenderer renderer(display); +ActivityManager activityManager(renderer, mappedInputManager); FontDecompressor fontDecompressor; -Activity* currentActivity; // Fonts EpdFont bookerly14RegularFont(&bookerly_14_regular); @@ -133,19 +125,6 @@ EpdFontFamily ui12FontFamily(&ui12RegularFont, &ui12BoldFont); unsigned long t1 = 0; unsigned long t2 = 0; -void exitActivity() { - if (currentActivity) { - currentActivity->onExit(); - delete currentActivity; - currentActivity = nullptr; - } -} - -void enterNewActivity(Activity* activity) { - currentActivity = activity; - currentActivity->onEnter(); -} - // Verify power button press duration on wake-up from deep sleep // Pre-condition: isWakeupByPowerButton() == true void verifyPowerButtonDuration() { @@ -201,10 +180,10 @@ void waitForPowerRelease() { // Enter deep sleep mode void enterDeepSleep() { HalPowerManager::Lock powerLock; // Ensure we are at normal CPU frequency for sleep preparation - APP_STATE.lastSleepFromReader = currentActivity && currentActivity->isReaderActivity(); + APP_STATE.lastSleepFromReader = activityManager.isReaderActivity(); APP_STATE.saveToFile(); - exitActivity(); - enterNewActivity(new SleepActivity(renderer, mappedInputManager)); + + activityManager.goToSleep(); display.deepSleep(); LOG_DBG("MAIN", "Power button press calibration value: %lu ms", t2 - t1); @@ -213,54 +192,10 @@ void enterDeepSleep() { powerManager.startDeepSleep(gpio); } -void onGoHome(); -void onGoToMyLibraryWithPath(const std::string& path); -void onGoToRecentBooks(); -void onGoToReader(const std::string& initialEpubPath) { - const std::string bookPath = initialEpubPath; // Copy before exitActivity() invalidates the reference - exitActivity(); - enterNewActivity(new ReaderActivity(renderer, mappedInputManager, bookPath, onGoHome, onGoToMyLibraryWithPath)); -} - -void onGoToFileTransfer() { - exitActivity(); - enterNewActivity(new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome)); -} - -void onGoToSettings() { - exitActivity(); - enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome)); -} - -void onGoToMyLibrary() { - exitActivity(); - enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader)); -} - -void onGoToRecentBooks() { - exitActivity(); - enterNewActivity(new RecentBooksActivity(renderer, mappedInputManager, onGoHome, onGoToReader)); -} - -void onGoToMyLibraryWithPath(const std::string& path) { - exitActivity(); - enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, path)); -} - -void onGoToBrowser() { - exitActivity(); - enterNewActivity(new OpdsBookBrowserActivity(renderer, mappedInputManager, onGoHome)); -} - -void onGoHome() { - exitActivity(); - enterNewActivity(new HomeActivity(renderer, mappedInputManager, onGoToReader, onGoToMyLibrary, onGoToRecentBooks, - onGoToSettings, onGoToFileTransfer, onGoToBrowser)); -} - void setupDisplayAndFonts() { display.begin(); renderer.begin(); + activityManager.begin(); LOG_DBG("MAIN", "Display initialized"); // Initialize font decompressor for compressed reader fonts @@ -310,8 +245,7 @@ void setup() { if (!Storage.begin()) { LOG_ERR("MAIN", "SD card initialization failed"); setupDisplayAndFonts(); - exitActivity(); - enterNewActivity(new FullScreenMessageActivity(renderer, mappedInputManager, "SD card error", EpdFontFamily::BOLD)); + activityManager.goToFullScreenMessage("SD card error", EpdFontFamily::BOLD); return; } @@ -344,8 +278,7 @@ void setup() { setupDisplayAndFonts(); - exitActivity(); - enterNewActivity(new BootActivity(renderer, mappedInputManager)); + activityManager.goToBoot(); APP_STATE.loadFromFile(); RECENT_BOOKS.loadFromFile(); @@ -354,14 +287,14 @@ void setup() { // crashed (indicated by readerActivityLoadCount > 0) if (APP_STATE.openEpubPath.empty() || !APP_STATE.lastSleepFromReader || mappedInputManager.isPressed(MappedInputManager::Button::Back) || APP_STATE.readerActivityLoadCount > 0) { - onGoHome(); + activityManager.goHome(); } else { // Clear app state to avoid getting into a boot loop if the epub doesn't load const auto path = APP_STATE.openEpubPath; APP_STATE.openEpubPath = ""; APP_STATE.readerActivityLoadCount++; APP_STATE.saveToFile(); - onGoToReader(path); + activityManager.goToReader(path); } // Ensure we're not still holding the power button before leaving setup @@ -401,7 +334,7 @@ void loop() { // Check for any user activity (button press or release) or active background work static unsigned long lastActivityTime = millis(); - if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || (currentActivity && currentActivity->preventAutoSleep())) { + if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || activityManager.preventAutoSleep()) { lastActivityTime = millis(); // Reset inactivity timer powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity } @@ -410,8 +343,8 @@ void loop() { if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.isPressed(HalGPIO::BTN_DOWN)) { if (screenshotButtonsReleased) { screenshotButtonsReleased = false; - if (currentActivity) { - Activity::RenderLock lock(*currentActivity); + { + RenderLock lock; ScreenshotUtil::takeScreenshot(renderer); } } @@ -439,9 +372,7 @@ void loop() { } const unsigned long activityStartTime = millis(); - if (currentActivity) { - currentActivity->loop(); - } + activityManager.loop(); const unsigned long activityDuration = millis() - activityStartTime; const unsigned long loopDuration = millis() - loopStartTime; @@ -455,7 +386,7 @@ void loop() { // Add delay at the end of the loop to prevent tight spinning // When an activity requests skip loop delay (e.g., webserver running), use yield() for faster response // Otherwise, use longer delay to save power - if (currentActivity && currentActivity->skipLoopDelay()) { + if (activityManager.skipLoopDelay()) { powerManager.setPowerSaving(false); // Make sure we're at full performance when skipLoopDelay is requested yield(); // Give FreeRTOS a chance to run tasks, but return immediately } else {