diff --git a/lib/I18n/I18nKeys.h b/lib/I18n/I18nKeys.h index 307f7e65..5ab759ce 100644 --- a/lib/I18n/I18nKeys.h +++ b/lib/I18n/I18nKeys.h @@ -427,6 +427,10 @@ enum class StrId : uint16_t { STR_SELECT_FOLDER, STR_DOWNLOAD_PATH, STR_POSITION, + STR_DOWNLOAD_COMPLETE, + STR_OPEN_BOOK, + STR_BACK_TO_LISTING, + STR_AFTER_DOWNLOAD, // Sentinel - must be last _COUNT }; diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 60d3b325..00202602 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -391,3 +391,7 @@ STR_SAVE_HERE: "Save Here" STR_SELECT_FOLDER: "Select Folder" STR_DOWNLOAD_PATH: "Download Path" STR_POSITION: "Position" +STR_DOWNLOAD_COMPLETE: "Download Complete!" +STR_OPEN_BOOK: "Open Book" +STR_BACK_TO_LISTING: "Back to Listing" +STR_AFTER_DOWNLOAD: "After Download" diff --git a/src/OpdsServerStore.cpp b/src/OpdsServerStore.cpp index fac4d9fb..af3ae869 100644 --- a/src/OpdsServerStore.cpp +++ b/src/OpdsServerStore.cpp @@ -83,6 +83,7 @@ bool OpdsServerStore::saveToFile() const { obj["password_obf"] = obfuscateToBase64(server.password); obj["download_path"] = server.downloadPath; obj["sort_order"] = server.sortOrder; + obj["after_download"] = server.afterDownloadAction; } String json; @@ -119,6 +120,7 @@ bool OpdsServerStore::loadFromFile() { } server.downloadPath = obj["download_path"] | std::string("/"); server.sortOrder = obj["sort_order"] | 0; + server.afterDownloadAction = obj["after_download"] | 0; if (server.sortOrder == 0) needsResave = true; servers.push_back(std::move(server)); } diff --git a/src/OpdsServerStore.h b/src/OpdsServerStore.h index 43acce14..899c8184 100644 --- a/src/OpdsServerStore.h +++ b/src/OpdsServerStore.h @@ -8,7 +8,8 @@ struct OpdsServer { std::string username; std::string password; // Plaintext in memory; obfuscated with hardware key on disk std::string downloadPath = "/"; - int sortOrder = 0; // Lower values appear first; ties broken alphabetically by name + int sortOrder = 0; // Lower values appear first; ties broken alphabetically by name + int afterDownloadAction = 0; // 0 = back to listing, 1 = open book }; /** diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index 1a58952e..daeace84 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -102,6 +102,45 @@ void OpdsBookBrowserActivity::loop() { return; } + // Handle download complete prompt + if (state == BrowserState::DOWNLOAD_COMPLETE) { + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + executePromptAction(0); + return; + } + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + executePromptAction(promptSelection); + return; + } + buttonNavigator.onNextRelease([this] { + countdownActive = false; + if (promptSelection != 1) { + promptSelection = 1; + requestUpdate(); + } + }); + buttonNavigator.onPreviousRelease([this] { + countdownActive = false; + if (promptSelection != 0) { + promptSelection = 0; + requestUpdate(); + } + }); + if (countdownActive) { + const unsigned long elapsed = millis() - downloadCompleteTime; + if (elapsed >= 5000) { + executePromptAction(server.afterDownloadAction); + return; + } + const int secondsLeft = static_cast((5000 - elapsed) / 1000); + if (secondsLeft != lastCountdownSecond) { + lastCountdownSecond = secondsLeft; + requestUpdate(); + } + } + return; + } + // Handle browsing state if (state == BrowserState::BROWSING) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { @@ -192,6 +231,37 @@ void OpdsBookBrowserActivity::render(Activity::RenderLock&&) { return; } + if (state == BrowserState::DOWNLOAD_COMPLETE) { + renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 50, tr(STR_DOWNLOAD_COMPLETE), true, EpdFontFamily::BOLD); + const auto maxWidth = pageWidth - 40; + auto title = renderer.truncatedText(UI_10_FONT_ID, statusMessage.c_str(), maxWidth); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, title.c_str()); + + const int buttonY = pageHeight / 2 + 20; + const char* backText = tr(STR_BACK_TO_LISTING); + const char* openText = tr(STR_OPEN_BOOK); + std::string backLabel = promptSelection == 0 ? "[" + std::string(backText) + "]" : std::string(backText); + std::string openLabel = promptSelection == 1 ? "[" + std::string(openText) + "]" : std::string(openText); + const int backWidth = renderer.getTextWidth(UI_10_FONT_ID, backLabel.c_str()); + const int openWidth = renderer.getTextWidth(UI_10_FONT_ID, openLabel.c_str()); + constexpr int buttonSpacing = 40; + const int totalWidth = backWidth + buttonSpacing + openWidth; + const int startX = (pageWidth - totalWidth) / 2; + renderer.drawText(UI_10_FONT_ID, startX, buttonY, backLabel.c_str()); + renderer.drawText(UI_10_FONT_ID, startX + backWidth + buttonSpacing, buttonY, openLabel.c_str()); + + if (countdownActive && lastCountdownSecond >= 0) { + char buf[8]; + snprintf(buf, sizeof(buf), "(%ds)", lastCountdownSecond + 1); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 50, buf); + } + + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_CONFIRM), tr(STR_DIR_LEFT), tr(STR_DIR_RIGHT)); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); + return; + } + // Browsing state // Show appropriate button hint based on selected entry type const char* confirmLabel = tr(STR_OPEN); @@ -369,7 +439,12 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book, const std::str epub.clearCache(); LOG_DBG("OPDS", "Cleared cache for: %s", filename.c_str()); - state = BrowserState::BROWSING; + downloadedFilePath = filename; + promptSelection = server.afterDownloadAction; + downloadCompleteTime = millis(); + countdownActive = true; + lastCountdownSecond = -1; + state = BrowserState::DOWNLOAD_COMPLETE; requestUpdate(); } else { state = BrowserState::ERROR; @@ -378,6 +453,15 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book, const std::str } } +void OpdsBookBrowserActivity::executePromptAction(int action) { + if (action == 1 && onGoToReader) { + onGoToReader(downloadedFilePath); + return; + } + state = BrowserState::BROWSING; + requestUpdate(); +} + void OpdsBookBrowserActivity::checkAndConnectWifi() { // Already connected? Verify connection is valid by checking IP if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) { diff --git a/src/activities/browser/OpdsBookBrowserActivity.h b/src/activities/browser/OpdsBookBrowserActivity.h index ea1fe850..c755c5ce 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.h +++ b/src/activities/browser/OpdsBookBrowserActivity.h @@ -23,12 +23,18 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity { BROWSING, // Displaying entries (navigation or books) PICKING_DIRECTORY, // Directory picker subactivity is active DOWNLOADING, // Downloading selected EPUB + DOWNLOAD_COMPLETE, // Prompt: open book or go back to listing ERROR // Error state with message }; explicit OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onGoHome, const OpdsServer& server) - : ActivityWithSubactivity("OpdsBookBrowser", renderer, mappedInput), onGoHome(onGoHome), server(server) {} + const std::function& onGoHome, + const std::function& onGoToReader, + const OpdsServer& server) + : ActivityWithSubactivity("OpdsBookBrowser", renderer, mappedInput), + onGoHome(onGoHome), + onGoToReader(onGoToReader), + server(server) {} void onEnter() override; void onExit() override; @@ -46,8 +52,14 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity { std::string statusMessage; size_t downloadProgress = 0; size_t downloadTotal = 0; + std::string downloadedFilePath; + unsigned long downloadCompleteTime = 0; + int promptSelection = 0; // 0 = back to listing, 1 = open book + bool countdownActive = false; + int lastCountdownSecond = -1; const std::function onGoHome; + const std::function onGoToReader; OpdsServer server; // Copied at construction — safe even if the store changes during browsing void checkAndConnectWifi(); @@ -60,6 +72,7 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity { void onDirectorySelected(const std::string& directory); void onDirectoryPickerCancelled(); void downloadBook(const OpdsEntry& book, const std::string& directory); + void executePromptAction(int action); bool preventAutoSleep() override { return true; } OpdsEntry pendingBook; diff --git a/src/activities/settings/OpdsSettingsActivity.cpp b/src/activities/settings/OpdsSettingsActivity.cpp index ba1fbb8e..f027b73e 100644 --- a/src/activities/settings/OpdsSettingsActivity.cpp +++ b/src/activities/settings/OpdsSettingsActivity.cpp @@ -15,9 +15,9 @@ #include "fontIds.h" namespace { -// Editable fields: Position, Name, URL, Username, Password, Download Path. +// Editable fields: Position, Name, URL, Username, Password, Download Path, After Download. // Existing servers also show a Delete option (BASE_ITEMS + 1). -constexpr int BASE_ITEMS = 6; +constexpr int BASE_ITEMS = 7; } // namespace int OpdsSettingsActivity::getMenuItemCount() const { @@ -185,7 +185,12 @@ void OpdsSettingsActivity::handleSelection() { requestUpdate(); }, editServer.downloadPath)); - } else if (selectedIndex == 6 && !isNewServer) { + } else if (selectedIndex == 6) { + // After Download — toggle between 0 (back to listing) and 1 (open book) + editServer.afterDownloadAction = editServer.afterDownloadAction == 0 ? 1 : 0; + saveServer(); + requestUpdate(); + } else if (selectedIndex == 7 && !isNewServer) { // Delete server OPDS_STORE.removeServer(static_cast(serverIndex)); onBack(); @@ -207,8 +212,9 @@ void OpdsSettingsActivity::render(Activity::RenderLock&&) { const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; const int menuItems = getMenuItemCount(); - const StrId fieldNames[] = {StrId::STR_POSITION, StrId::STR_SERVER_NAME, StrId::STR_OPDS_SERVER_URL, - StrId::STR_USERNAME, StrId::STR_PASSWORD, StrId::STR_DOWNLOAD_PATH}; + const StrId fieldNames[] = {StrId::STR_POSITION, StrId::STR_SERVER_NAME, StrId::STR_OPDS_SERVER_URL, + StrId::STR_USERNAME, StrId::STR_PASSWORD, StrId::STR_DOWNLOAD_PATH, + StrId::STR_AFTER_DOWNLOAD}; GUI.drawList( renderer, Rect{0, contentTop, pageWidth, contentHeight}, menuItems, static_cast(selectedIndex), @@ -232,6 +238,8 @@ void OpdsSettingsActivity::render(Activity::RenderLock&&) { return editServer.password.empty() ? std::string(tr(STR_NOT_SET)) : std::string("******"); } else if (index == 5) { return editServer.downloadPath; + } else if (index == 6) { + return std::string(editServer.afterDownloadAction == 0 ? tr(STR_BACK_TO_LISTING) : tr(STR_OPEN_BOOK)); } return std::string(""); }, diff --git a/src/main.cpp b/src/main.cpp index 3c0138b9..a3b8e843 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -265,13 +265,13 @@ void onGoToBrowser() { exitActivity(); const auto& servers = OPDS_STORE.getServers(); if (servers.size() == 1) { - enterNewActivity(new OpdsBookBrowserActivity(renderer, mappedInputManager, onGoHome, servers[0])); + enterNewActivity(new OpdsBookBrowserActivity(renderer, mappedInputManager, onGoHome, onGoToReader, servers[0])); } else { enterNewActivity(new OpdsServerListActivity(renderer, mappedInputManager, onGoHome, [](size_t serverIndex) { const auto* server = OPDS_STORE.getServer(serverIndex); if (server) { exitActivity(); - enterNewActivity(new OpdsBookBrowserActivity(renderer, mappedInputManager, onGoHome, *server)); + enterNewActivity(new OpdsBookBrowserActivity(renderer, mappedInputManager, onGoHome, onGoToReader, *server)); } })); }