diff --git a/src/activities/home/BookManageMenuActivity.h b/src/activities/home/BookManageMenuActivity.h index a4e6ec95..300f2f37 100644 --- a/src/activities/home/BookManageMenuActivity.h +++ b/src/activities/home/BookManageMenuActivity.h @@ -23,10 +23,12 @@ class BookManageMenuActivity final : public Activity { explicit BookManageMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& bookPath, bool isArchived, const std::function& onAction, - const std::function& onCancel) + const std::function& onCancel, + bool initialSkipRelease = false) : Activity("BookManageMenu", renderer, mappedInput), bookPath(bookPath), archived(isArchived), + ignoreNextConfirmRelease(initialSkipRelease), onAction(onAction), onCancel(onCancel) { buildMenuItems(); @@ -49,7 +51,7 @@ class BookManageMenuActivity final : public Activity { int selectedIndex = 0; ButtonNavigator buttonNavigator; - bool ignoreNextConfirmRelease = false; + bool ignoreNextConfirmRelease; static constexpr unsigned long LONG_PRESS_MS = 700; const std::function onAction; diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 57538746..6e79810d 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -222,7 +222,7 @@ void HomeActivity::loop() { if (menuSelectedIndex == 0) { // Long-press on Browse Files → go to archive folder ignoreNextConfirmRelease = true; - onMyLibraryOpenWithPath("/.archive"); + onMyLibraryOpenWithPath("/.archive", true); return; } } @@ -339,14 +339,19 @@ void HomeActivity::openManageMenu(const std::string& bookPath) { GUI.drawPopup(renderer, success ? tr(STR_DONE) : tr(STR_ACTION_FAILED)); } requestUpdateAndWait(); - // Reload recents since the book may have been removed/archived + // Fully reset recent books state so the home screen reloads cleanly recentBooks.clear(); recentsLoaded = false; + recentsLoading = false; coverRendered = false; + freeCoverBuffer(); + selectorIndex = 0; + firstRenderDone = false; requestUpdate(); }, [this] { exitActivity(); requestUpdate(); - })); + }, + true)); } diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index 6b2a3ba8..ca0d43f4 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -27,7 +27,7 @@ class HomeActivity final : public ActivityWithSubactivity { const std::function onSelectBook; const std::function onMyLibraryOpen; - const std::function onMyLibraryOpenWithPath; + const std::function onMyLibraryOpenWithPath; const std::function onRecentsOpen; const std::function onSettingsOpen; const std::function onFileTransferOpen; @@ -45,7 +45,7 @@ class HomeActivity final : public ActivityWithSubactivity { explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::function& onSelectBook, const std::function& onMyLibraryOpen, - const std::function& onMyLibraryOpenWithPath, + const std::function& onMyLibraryOpenWithPath, const std::function& onRecentsOpen, const std::function& onSettingsOpen, const std::function& onFileTransferOpen, const std::function& onOpdsBrowserOpen) diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 319b3b6b..5c2dd874 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -124,6 +124,16 @@ void MyLibraryActivity::loop() { return; } + // Deferred open: wait for Confirm release before navigating to avoid stale event in reader + if (!pendingOpenPath.empty()) { + if (!mappedInput.isPressed(MappedInputManager::Button::Confirm)) { + std::string path = std::move(pendingOpenPath); + pendingOpenPath.clear(); + onSelectBook(path); + } + return; + } + // Long press BACK (1s+) goes to root folder if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS && basepath != "/") { @@ -135,15 +145,21 @@ void MyLibraryActivity::loop() { const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false); - // Long-press Confirm: open manage menu for book files + // In archive context: long-press = unarchive+open, short-press = manage menu + // Outside archive: long-press = manage menu, short-press = open + const bool inArchive = isInArchive(); + const bool isBookFile = !files.empty() && selectorIndex < files.size() && files[selectorIndex].back() != '/'; + if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= LONG_PRESS_MS && - !ignoreNextConfirmRelease) { - if (!files.empty() && selectorIndex < files.size() && files[selectorIndex].back() != '/') { - ignoreNextConfirmRelease = true; - const std::string fullPath = (basepath.back() == '/' ? basepath : basepath + "/") + files[selectorIndex]; + !ignoreNextConfirmRelease && isBookFile) { + ignoreNextConfirmRelease = true; + const std::string fullPath = (basepath.back() == '/' ? basepath : basepath + "/") + files[selectorIndex]; + if (inArchive) { + unarchiveAndOpen(fullPath); + } else { openManageMenu(fullPath); - return; } + return; } if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { @@ -161,6 +177,9 @@ void MyLibraryActivity::loop() { loadFiles(); selectorIndex = 0; requestUpdate(); + } else if (inArchive) { + const std::string fullPath = basepath + files[selectorIndex]; + openManageMenu(fullPath); } else { onSelectBook(basepath + files[selectorIndex]); return; @@ -259,6 +278,7 @@ void MyLibraryActivity::render(Activity::RenderLock&&) { void MyLibraryActivity::openManageMenu(const std::string& bookPath) { const bool isArchived = BookManager::isArchived(bookPath); + const bool fromLongPress = !isInArchive(); const std::string capturedPath = bookPath; enterNewActivity(new BookManageMenuActivity( renderer, mappedInput, capturedPath, isArchived, @@ -285,13 +305,28 @@ void MyLibraryActivity::openManageMenu(const std::string& bookPath) { success = BookManager::reindexBook(capturedPath, true); break; } + if (success && BookManager::isArchived(capturedPath) && + (action == BookManageMenuActivity::Action::UNARCHIVE || + action == BookManageMenuActivity::Action::DELETE)) { + BookManager::cleanupEmptyArchiveDirs(capturedPath); + } { RenderLock lock(*this); GUI.drawPopup(renderer, success ? tr(STR_DONE) : tr(STR_ACTION_FAILED)); } requestUpdateAndWait(); loadFiles(); - if (selectorIndex >= files.size() && !files.empty()) { + if (files.empty() && isInArchive() && basepath != "/.archive") { + // Current directory was removed; navigate up to nearest existing ancestor + while (basepath.length() > std::string("/.archive").length()) { + auto slash = basepath.find_last_of('/'); + if (slash == std::string::npos || slash == 0) break; + basepath = basepath.substr(0, slash); + loadFiles(); + if (!files.empty() || basepath == "/.archive") break; + } + selectorIndex = 0; + } else if (selectorIndex >= files.size() && !files.empty()) { selectorIndex = files.size() - 1; } requestUpdate(); @@ -299,7 +334,34 @@ void MyLibraryActivity::openManageMenu(const std::string& bookPath) { [this] { exitActivity(); requestUpdate(); - })); + }, + fromLongPress)); +} + +bool MyLibraryActivity::isInArchive() const { return basepath.rfind("/.archive", 0) == 0; } + +void MyLibraryActivity::unarchiveAndOpen(const std::string& bookPath) { + std::string unarchivedPath; + if (BookManager::unarchiveBook(bookPath, &unarchivedPath)) { + BookManager::cleanupEmptyArchiveDirs(bookPath); + { + RenderLock lock(*this); + GUI.drawPopup(renderer, tr(STR_BOOK_UNARCHIVED)); + } + requestUpdateAndWait(); + pendingOpenPath = unarchivedPath; + } else { + { + RenderLock lock(*this); + GUI.drawPopup(renderer, tr(STR_ACTION_FAILED)); + } + requestUpdateAndWait(); + loadFiles(); + if (selectorIndex >= files.size() && !files.empty()) { + selectorIndex = files.size() - 1; + } + requestUpdate(); + } } size_t MyLibraryActivity::findEntry(const std::string& name) const { diff --git a/src/activities/home/MyLibraryActivity.h b/src/activities/home/MyLibraryActivity.h index b0df420f..bebd3790 100644 --- a/src/activities/home/MyLibraryActivity.h +++ b/src/activities/home/MyLibraryActivity.h @@ -21,6 +21,9 @@ class MyLibraryActivity final : public ActivityWithSubactivity { bool ignoreNextConfirmRelease = false; static constexpr unsigned long LONG_PRESS_MS = 700; + // Deferred open: wait for Confirm release before navigating to book + std::string pendingOpenPath; + // Callbacks const std::function onSelectBook; const std::function onGoHome; @@ -28,16 +31,19 @@ class MyLibraryActivity final : public ActivityWithSubactivity { // Data loading void loadFiles(); size_t findEntry(const std::string& name) const; + bool isInArchive() const; void openManageMenu(const std::string& bookPath); + void unarchiveAndOpen(const std::string& bookPath); public: explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::function& onGoHome, const std::function& onSelectBook, - std::string initialPath = "/") + std::string initialPath = "/", bool initialSkipRelease = false) : ActivityWithSubactivity("MyLibrary", renderer, mappedInput), basepath(initialPath.empty() ? "/" : std::move(initialPath)), + ignoreNextConfirmRelease(initialSkipRelease), onSelectBook(onSelectBook), onGoHome(onGoHome) {} void onEnter() override; diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index ae72321e..4f9a573f 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -175,5 +175,6 @@ void RecentBooksActivity::openManageMenu(const std::string& bookPath) { [this] { exitActivity(); requestUpdate(); - })); + }, + true)); } diff --git a/src/activities/reader/EndOfBookMenuActivity.cpp b/src/activities/reader/EndOfBookMenuActivity.cpp index f50af8eb..8d7af63f 100644 --- a/src/activities/reader/EndOfBookMenuActivity.cpp +++ b/src/activities/reader/EndOfBookMenuActivity.cpp @@ -11,6 +11,7 @@ void EndOfBookMenuActivity::buildMenuItems() { menuItems.clear(); menuItems.push_back({Action::ARCHIVE, StrId::STR_ARCHIVE_BOOK}); menuItems.push_back({Action::DELETE, StrId::STR_DELETE_BOOK}); + menuItems.push_back({Action::TABLE_OF_CONTENTS, StrId::STR_TABLE_OF_CONTENTS}); menuItems.push_back({Action::BACK_TO_BEGINNING, StrId::STR_BACK_TO_BEGINNING}); menuItems.push_back({Action::CLOSE_BOOK, StrId::STR_CLOSE_BOOK}); menuItems.push_back({Action::CLOSE_MENU, StrId::STR_CLOSE_MENU}); @@ -55,42 +56,18 @@ void EndOfBookMenuActivity::render(Activity::RenderLock&&) { const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); + auto metrics = UITheme::getInstance().getMetrics(); - constexpr int popupMargin = 20; - constexpr int lineHeight = 30; - constexpr int titleHeight = 40; - const int optionCount = static_cast(menuItems.size()); - const int popupH = titleHeight + popupMargin + lineHeight * optionCount + popupMargin; - const int popupW = pageWidth - 60; - const int popupX = (pageWidth - popupW) / 2; - const int popupY = (pageHeight - popupH) / 2; + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_END_OF_BOOK)); - // Popup border and background - renderer.fillRect(popupX - 2, popupY - 2, popupW + 4, popupH + 4, true); - renderer.fillRect(popupX, popupY, popupW, popupH, false); + const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing; - // Title - renderer.drawText(UI_12_FONT_ID, popupX + popupMargin, popupY + 8, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD); + GUI.drawList( + renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(menuItems.size()), selectedIndex, + [this](int index) { return std::string(I18N.get(menuItems[index].labelId)); }); - // Divider line - const int dividerY = popupY + titleHeight; - renderer.fillRect(popupX + 4, dividerY, popupW - 8, 1, true); - - // Menu items - const int startY = dividerY + popupMargin / 2; - for (int i = 0; i < optionCount; ++i) { - const int itemY = startY + i * lineHeight; - const bool isSelected = (i == selectedIndex); - - if (isSelected) { - renderer.fillRect(popupX + 2, itemY, popupW - 4, lineHeight, true); - } - - renderer.drawText(UI_10_FONT_ID, popupX + popupMargin, itemY, I18N.get(menuItems[i].labelId), !isSelected); - } - - // Button hints - const auto labels = mappedInput.mapLabels(tr(STR_CLOSE_MENU), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); + const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); diff --git a/src/activities/reader/EndOfBookMenuActivity.h b/src/activities/reader/EndOfBookMenuActivity.h index dc0af064..003eec7d 100644 --- a/src/activities/reader/EndOfBookMenuActivity.h +++ b/src/activities/reader/EndOfBookMenuActivity.h @@ -14,6 +14,7 @@ class EndOfBookMenuActivity final : public Activity { enum class Action { ARCHIVE, DELETE, + TABLE_OF_CONTENTS, BACK_TO_BEGINNING, CLOSE_BOOK, CLOSE_MENU, diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 668825c6..49af1855 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -222,6 +222,52 @@ void EpubReaderActivity::loop() { return; // Don't access 'this' after callback } + // Deferred end-of-book menu (set in render() to avoid deadlock) + if (pendingEndOfBookMenu) { + pendingEndOfBookMenu = false; + endOfBookMenuOpened = true; + const std::string path = epub->getPath(); + enterNewActivity(new EndOfBookMenuActivity( + renderer, mappedInput, path, [this](EndOfBookMenuActivity::Action action) { + exitActivity(); + switch (action) { + case EndOfBookMenuActivity::Action::ARCHIVE: + if (epub) BookManager::archiveBook(epub->getPath()); + pendingGoHome = true; + break; + case EndOfBookMenuActivity::Action::DELETE: + if (epub) BookManager::deleteBook(epub->getPath()); + pendingGoHome = true; + break; + case EndOfBookMenuActivity::Action::TABLE_OF_CONTENTS: + endOfBookMenuOpened = false; + currentSpineIndex = epub->getSpineItemsCount() - 1; + nextPageNumber = UINT16_MAX; + section.reset(); + openChapterSelection(); + break; + case EndOfBookMenuActivity::Action::BACK_TO_BEGINNING: + currentSpineIndex = 0; + nextPageNumber = 0; + section.reset(); + endOfBookMenuOpened = false; + requestUpdate(); + break; + case EndOfBookMenuActivity::Action::CLOSE_BOOK: + pendingGoHome = true; + break; + case EndOfBookMenuActivity::Action::CLOSE_MENU: + currentSpineIndex = epub->getSpineItemsCount() - 1; + nextPageNumber = UINT16_MAX; + section.reset(); + endOfBookMenuOpened = false; + requestUpdate(); + break; + } + })); + return; + } + // 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 @@ -840,42 +886,10 @@ void EpubReaderActivity::render(Activity::RenderLock&& lock) { currentSpineIndex = epub->getSpineItemsCount(); } - // Show end of book screen + // End of book — defer menu creation to loop() to avoid deadlock (render holds the lock) if (currentSpineIndex == epub->getSpineItemsCount()) { if (!endOfBookMenuOpened) { - endOfBookMenuOpened = true; - const std::string path = epub->getPath(); - enterNewActivity(new EndOfBookMenuActivity( - renderer, mappedInput, path, [this](EndOfBookMenuActivity::Action action) { - exitActivity(); - switch (action) { - case EndOfBookMenuActivity::Action::ARCHIVE: - if (epub) BookManager::archiveBook(epub->getPath()); - pendingGoHome = true; - break; - case EndOfBookMenuActivity::Action::DELETE: - if (epub) BookManager::deleteBook(epub->getPath()); - pendingGoHome = true; - break; - case EndOfBookMenuActivity::Action::BACK_TO_BEGINNING: - currentSpineIndex = 0; - nextPageNumber = 0; - section.reset(); - endOfBookMenuOpened = false; - requestUpdate(); - break; - case EndOfBookMenuActivity::Action::CLOSE_BOOK: - pendingGoHome = true; - break; - case EndOfBookMenuActivity::Action::CLOSE_MENU: - currentSpineIndex = epub->getSpineItemsCount() - 1; - nextPageNumber = UINT16_MAX; - section.reset(); - endOfBookMenuOpened = false; - requestUpdate(); - break; - } - })); + pendingEndOfBookMenu = true; } return; } diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 804e6fe1..a9673851 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -28,6 +28,7 @@ class EpubReaderActivity final : public ActivityWithSubactivity { bool silentIndexingActive = false; // True while silently pre-indexing the next chapter int preIndexedNextSpine = -1; // Spine index already pre-indexed (prevents re-render loop) bool endOfBookMenuOpened = false; // Guard to prevent repeated opening of EndOfBookMenuActivity + bool pendingEndOfBookMenu = false; // Deferred: open EndOfBookMenuActivity from loop(), not render() const std::function onGoBack; const std::function onGoHome; diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 974ec6ec..7a5f520c 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -176,6 +176,10 @@ void TxtReaderActivity::loop() { if (txt) BookManager::deleteBook(txt->getPath()); if (onGoHome) onGoHome(); return; + case EndOfBookMenuActivity::Action::TABLE_OF_CONTENTS: + endOfBookMenuOpened = false; + requestUpdate(); + break; case EndOfBookMenuActivity::Action::BACK_TO_BEGINNING: currentPage = 0; endOfBookMenuOpened = false; diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index eeab0afa..42954b19 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -106,6 +106,60 @@ void XtcReaderActivity::loop() { return; } + // Deferred end-of-book menu (set in render() to avoid deadlock) + if (pendingEndOfBookMenu) { + pendingEndOfBookMenu = false; + endOfBookMenuOpened = true; + const std::string path = xtc->getPath(); + enterNewActivity(new EndOfBookMenuActivity( + renderer, mappedInput, path, [this](EndOfBookMenuActivity::Action action) { + exitActivity(); + switch (action) { + case EndOfBookMenuActivity::Action::ARCHIVE: + if (xtc) BookManager::archiveBook(xtc->getPath()); + if (onGoHome) onGoHome(); + break; + case EndOfBookMenuActivity::Action::DELETE: + if (xtc) BookManager::deleteBook(xtc->getPath()); + if (onGoHome) onGoHome(); + break; + case EndOfBookMenuActivity::Action::TABLE_OF_CONTENTS: + endOfBookMenuOpened = false; + currentPage = xtc->getPageCount() - 1; + if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) { + enterNewActivity(new XtcReaderChapterSelectionActivity( + renderer, mappedInput, xtc, currentPage, + [this] { + exitActivity(); + requestUpdate(); + }, + [this](const uint32_t newPage) { + currentPage = newPage; + exitActivity(); + requestUpdate(); + })); + } else { + requestUpdate(); + } + break; + case EndOfBookMenuActivity::Action::BACK_TO_BEGINNING: + currentPage = 0; + endOfBookMenuOpened = false; + requestUpdate(); + break; + case EndOfBookMenuActivity::Action::CLOSE_BOOK: + if (onGoHome) onGoHome(); + break; + case EndOfBookMenuActivity::Action::CLOSE_MENU: + currentPage = xtc->getPageCount() - 1; + endOfBookMenuOpened = false; + requestUpdate(); + break; + } + })); + return; + } + // Enter chapter selection activity if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) { @@ -186,38 +240,10 @@ void XtcReaderActivity::render(Activity::RenderLock&&) { return; } - // Bounds check - end of book + // End of book — defer menu creation to loop() to avoid deadlock (render holds the lock) if (currentPage >= xtc->getPageCount()) { if (!endOfBookMenuOpened) { - endOfBookMenuOpened = true; - const std::string path = xtc->getPath(); - enterNewActivity(new EndOfBookMenuActivity( - renderer, mappedInput, path, [this](EndOfBookMenuActivity::Action action) { - exitActivity(); - switch (action) { - case EndOfBookMenuActivity::Action::ARCHIVE: - if (xtc) BookManager::archiveBook(xtc->getPath()); - if (onGoHome) onGoHome(); - break; - case EndOfBookMenuActivity::Action::DELETE: - if (xtc) BookManager::deleteBook(xtc->getPath()); - if (onGoHome) onGoHome(); - break; - case EndOfBookMenuActivity::Action::BACK_TO_BEGINNING: - currentPage = 0; - endOfBookMenuOpened = false; - requestUpdate(); - break; - case EndOfBookMenuActivity::Action::CLOSE_BOOK: - if (onGoHome) onGoHome(); - break; - case EndOfBookMenuActivity::Action::CLOSE_MENU: - currentPage = xtc->getPageCount() - 1; - endOfBookMenuOpened = false; - requestUpdate(); - break; - } - })); + pendingEndOfBookMenu = true; } return; } diff --git a/src/activities/reader/XtcReaderActivity.h b/src/activities/reader/XtcReaderActivity.h index 73d8cd33..ca42c613 100644 --- a/src/activities/reader/XtcReaderActivity.h +++ b/src/activities/reader/XtcReaderActivity.h @@ -18,6 +18,7 @@ class XtcReaderActivity final : public ActivityWithSubactivity { int pagesUntilFullRefresh = 0; bool endOfBookMenuOpened = false; + bool pendingEndOfBookMenu = false; const std::function onGoBack; const std::function onGoHome; diff --git a/src/main.cpp b/src/main.cpp index adfef5d4..5e183183 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -224,12 +224,13 @@ void enterDeepSleep() { } void onGoHome(); -void onGoToMyLibraryWithPath(const std::string& path); +void onGoToMyLibraryWithPath(const std::string& path, bool initialSkipRelease = false); 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)); + enterNewActivity(new ReaderActivity(renderer, mappedInputManager, bookPath, onGoHome, + [](const std::string& p) { onGoToMyLibraryWithPath(p); })); } void onGoToFileTransfer() { @@ -252,9 +253,9 @@ void onGoToRecentBooks() { enterNewActivity(new RecentBooksActivity(renderer, mappedInputManager, onGoHome, onGoToReader)); } -void onGoToMyLibraryWithPath(const std::string& path) { +void onGoToMyLibraryWithPath(const std::string& path, bool initialSkipRelease) { exitActivity(); - enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, path)); + enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, path, initialSkipRelease)); } void onGoToBrowser() { diff --git a/src/util/BookManager.cpp b/src/util/BookManager.cpp index 3edb9fcc..77d49abf 100644 --- a/src/util/BookManager.cpp +++ b/src/util/BookManager.cpp @@ -100,7 +100,7 @@ bool archiveBook(const std::string& bookPath) { return true; } -bool unarchiveBook(const std::string& archivePath) { +bool unarchiveBook(const std::string& archivePath, std::string* unarchivedPath) { if (!isArchived(archivePath)) { LOG_ERR("BKMGR", "Book is not in archive: %s", archivePath.c_str()); return false; @@ -139,6 +139,7 @@ bool unarchiveBook(const std::string& archivePath) { RECENT_BOOKS.removeBook(archivePath); LOG_DBG("BKMGR", "Unarchived: %s -> %s", archivePath.c_str(), destPath.c_str()); + if (unarchivedPath) *unarchivedPath = destPath; return true; } @@ -222,4 +223,33 @@ bool reindexBook(const std::string& bookPath, bool alsoRegenerateCovers) { return true; } +void cleanupEmptyArchiveDirs(const std::string& bookPath) { + if (!isArchived(bookPath)) return; + + // Walk up from the book's parent directory, removing empty dirs + std::string dir = bookPath.substr(0, bookPath.find_last_of('/')); + const std::string archiveRoot(ARCHIVE_ROOT); + + while (dir.length() > archiveRoot.length()) { + auto d = Storage.open(dir.c_str()); + if (!d || !d.isDirectory()) { + if (d) d.close(); + break; + } + auto child = d.openNextFile(); + const bool empty = !child; + if (child) child.close(); + d.close(); + + if (!empty) break; + + Storage.rmdir(dir.c_str()); + LOG_DBG("BKMGR", "Removed empty archive dir: %s", dir.c_str()); + + auto slash = dir.find_last_of('/'); + if (slash == std::string::npos || slash == 0) break; + dir = dir.substr(0, slash); + } +} + } // namespace BookManager diff --git a/src/util/BookManager.h b/src/util/BookManager.h index 47057232..8de019da 100644 --- a/src/util/BookManager.h +++ b/src/util/BookManager.h @@ -15,7 +15,8 @@ bool archiveBook(const std::string& bookPath); // Move a book from /.archive/ back to its original location. // Falls back to "/" if the original directory no longer exists. // Renames the cache dir to match the restored path hash. Returns true on success. -bool unarchiveBook(const std::string& archivePath); +// If unarchivedPath is non-null, stores the destination path on success. +bool unarchiveBook(const std::string& archivePath, std::string* unarchivedPath = nullptr); // Delete a book file, its cache directory, and remove from recents. bool deleteBook(const std::string& bookPath); @@ -31,4 +32,8 @@ bool reindexBook(const std::string& bookPath, bool alsoRegenerateCovers); // Returns true if the book path is inside the /.archive/ folder. bool isArchived(const std::string& bookPath); +// Remove empty directories under /.archive/ walking up from the book's parent. +// Stops at /.archive itself (never removes it). +void cleanupEmptyArchiveDirs(const std::string& bookPath); + } // namespace BookManager