fix: resolve end-of-book deadlock, long-press guards, archive UX, and home screen refresh
- Fix device freeze at end-of-book by deferring EndOfBookMenuActivity creation from render() to loop() (avoids RenderLock deadlock) in EpubReaderActivity and XtcReaderActivity - Add initialSkipRelease to BookManageMenuActivity to prevent stale Confirm release from triggering actions when opened via long-press - Add initialSkipRelease to MyLibraryActivity for long-press Browse Files -> archive navigation - Thread skip-release through HomeActivity callback and main.cpp - Fix HomeActivity stale cover buffer after archive/delete by fully resetting render state (freeCoverBuffer, firstRenderDone, etc.) - Swap short/long-press actions in .archive context: short-press opens manage menu, long-press unarchives and opens the book - Add deferred open pattern (pendingOpenPath) to wait for Confirm release before navigating to reader after unarchive - Add BookManager::cleanupEmptyArchiveDirs() to remove empty parent directories after unarchive/delete inside .archive - Add optional unarchivedPath output parameter to BookManager::unarchiveBook - Restyle EndOfBookMenuActivity to standard list layout with proper header, margins, and button hints matching other screens - Change EndOfBookMenuActivity back button hint to "« Back" - Add Table of Contents option to EndOfBookMenuActivity Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -23,10 +23,12 @@ class BookManageMenuActivity final : public Activity {
|
||||
explicit BookManageMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::string& bookPath, bool isArchived,
|
||||
const std::function<void(Action)>& onAction,
|
||||
const std::function<void()>& onCancel)
|
||||
const std::function<void()>& 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<void(Action)> onAction;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class HomeActivity final : public ActivityWithSubactivity {
|
||||
|
||||
const std::function<void(const std::string& path)> onSelectBook;
|
||||
const std::function<void()> onMyLibraryOpen;
|
||||
const std::function<void(const std::string& path)> onMyLibraryOpenWithPath;
|
||||
const std::function<void(const std::string& path, bool initialSkipRelease)> onMyLibraryOpenWithPath;
|
||||
const std::function<void()> onRecentsOpen;
|
||||
const std::function<void()> onSettingsOpen;
|
||||
const std::function<void()> onFileTransferOpen;
|
||||
@@ -45,7 +45,7 @@ class HomeActivity final : public ActivityWithSubactivity {
|
||||
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void(const std::string& path)>& onSelectBook,
|
||||
const std::function<void()>& onMyLibraryOpen,
|
||||
const std::function<void(const std::string& path)>& onMyLibraryOpenWithPath,
|
||||
const std::function<void(const std::string& path, bool initialSkipRelease)>& onMyLibraryOpenWithPath,
|
||||
const std::function<void()>& onRecentsOpen,
|
||||
const std::function<void()>& onSettingsOpen, const std::function<void()>& onFileTransferOpen,
|
||||
const std::function<void()>& onOpdsBrowserOpen)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<void(const std::string& path)> onSelectBook;
|
||||
const std::function<void()> 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<void()>& onGoHome,
|
||||
const std::function<void(const std::string& path)>& 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;
|
||||
|
||||
@@ -175,5 +175,6 @@ void RecentBooksActivity::openManageMenu(const std::string& bookPath) {
|
||||
[this] {
|
||||
exitActivity();
|
||||
requestUpdate();
|
||||
}));
|
||||
},
|
||||
true));
|
||||
}
|
||||
|
||||
@@ -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<int>(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<int>(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();
|
||||
|
||||
@@ -14,6 +14,7 @@ class EndOfBookMenuActivity final : public Activity {
|
||||
enum class Action {
|
||||
ARCHIVE,
|
||||
DELETE,
|
||||
TABLE_OF_CONTENTS,
|
||||
BACK_TO_BEGINNING,
|
||||
CLOSE_BOOK,
|
||||
CLOSE_MENU,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<void()> onGoBack;
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ class XtcReaderActivity final : public ActivityWithSubactivity {
|
||||
int pagesUntilFullRefresh = 0;
|
||||
|
||||
bool endOfBookMenuOpened = false;
|
||||
bool pendingEndOfBookMenu = false;
|
||||
const std::function<void()> onGoBack;
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user