Compare commits
13 Commits
4dadea1a03
...
0e2440aea8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e2440aea8
|
||
|
|
39ef1e6d78
|
||
|
|
3cc127d658
|
||
|
|
98146f2545
|
||
|
|
f5b708424d
|
||
|
|
1c19899aa3
|
||
|
|
390f10f30d
|
||
|
|
49471e36f1
|
||
|
|
c44ac0272a
|
||
|
|
29954a3683
|
||
|
|
3eddb07a1a
|
||
|
|
f443f5dde0
|
||
|
|
3d51dfeeb7
|
@@ -398,6 +398,23 @@ enum class StrId : uint16_t {
|
||||
STR_INDEXING_POPUP,
|
||||
STR_INDEXING_STATUS_TEXT,
|
||||
STR_INDEXING_STATUS_ICON,
|
||||
STR_SYNC_CLOCK,
|
||||
STR_TIME_SYNCED,
|
||||
STR_MANAGE_BOOK,
|
||||
STR_ARCHIVE_BOOK,
|
||||
STR_UNARCHIVE_BOOK,
|
||||
STR_DELETE_BOOK,
|
||||
STR_DELETE_CACHE_ONLY,
|
||||
STR_REINDEX_BOOK,
|
||||
STR_BROWSE_ARCHIVE,
|
||||
STR_BOOK_ARCHIVED,
|
||||
STR_BOOK_UNARCHIVED,
|
||||
STR_BOOK_DELETED,
|
||||
STR_CACHE_DELETED,
|
||||
STR_BOOK_REINDEXED,
|
||||
STR_ACTION_FAILED,
|
||||
STR_BACK_TO_BEGINNING,
|
||||
STR_CLOSE_MENU,
|
||||
// Sentinel - must be last
|
||||
_COUNT
|
||||
};
|
||||
|
||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Zobrazení indexování"
|
||||
STR_INDEXING_POPUP: "Popup"
|
||||
STR_INDEXING_STATUS_TEXT: "Text stavového řádku"
|
||||
STR_INDEXING_STATUS_ICON: "Ikona stavového řádku"
|
||||
STR_SYNC_CLOCK: "Sync Clock"
|
||||
STR_TIME_SYNCED: "Time synced!"
|
||||
|
||||
@@ -362,3 +362,20 @@ STR_INDEXING_DISPLAY: "Indexing Display"
|
||||
STR_INDEXING_POPUP: "Popup"
|
||||
STR_INDEXING_STATUS_TEXT: "Status Bar Text"
|
||||
STR_INDEXING_STATUS_ICON: "Status Bar Icon"
|
||||
STR_SYNC_CLOCK: "Sync Clock"
|
||||
STR_TIME_SYNCED: "Time synced!"
|
||||
STR_MANAGE_BOOK: "Manage Book"
|
||||
STR_ARCHIVE_BOOK: "Archive Book"
|
||||
STR_UNARCHIVE_BOOK: "Unarchive Book"
|
||||
STR_DELETE_BOOK: "Delete Book"
|
||||
STR_DELETE_CACHE_ONLY: "Delete Cache Only"
|
||||
STR_REINDEX_BOOK: "Reindex Book"
|
||||
STR_BROWSE_ARCHIVE: "Browse Archive"
|
||||
STR_BOOK_ARCHIVED: "Book archived"
|
||||
STR_BOOK_UNARCHIVED: "Book unarchived"
|
||||
STR_BOOK_DELETED: "Book deleted"
|
||||
STR_CACHE_DELETED: "Cache deleted"
|
||||
STR_BOOK_REINDEXED: "Book reindexed"
|
||||
STR_ACTION_FAILED: "Action failed"
|
||||
STR_BACK_TO_BEGINNING: "Back to Beginning"
|
||||
STR_CLOSE_MENU: "Close Menu"
|
||||
|
||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Affichage indexation"
|
||||
STR_INDEXING_POPUP: "Popup"
|
||||
STR_INDEXING_STATUS_TEXT: "Texte barre d'état"
|
||||
STR_INDEXING_STATUS_ICON: "Icône barre d'état"
|
||||
STR_SYNC_CLOCK: "Sync Clock"
|
||||
STR_TIME_SYNCED: "Time synced!"
|
||||
|
||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Indexierungsanzeige"
|
||||
STR_INDEXING_POPUP: "Popup"
|
||||
STR_INDEXING_STATUS_TEXT: "Statusleistentext"
|
||||
STR_INDEXING_STATUS_ICON: "Statusleistensymbol"
|
||||
STR_SYNC_CLOCK: "Sync Clock"
|
||||
STR_TIME_SYNCED: "Time synced!"
|
||||
|
||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Exibição de indexação"
|
||||
STR_INDEXING_POPUP: "Popup"
|
||||
STR_INDEXING_STATUS_TEXT: "Texto da barra"
|
||||
STR_INDEXING_STATUS_ICON: "Ícone da barra"
|
||||
STR_SYNC_CLOCK: "Sync Clock"
|
||||
STR_TIME_SYNCED: "Time synced!"
|
||||
|
||||
@@ -316,3 +316,5 @@ STR_UPLOAD: "Încărcare"
|
||||
STR_BOOK_S_STYLE: "Stilul cărţii"
|
||||
STR_EMBEDDED_STYLE: "Stil încorporat"
|
||||
STR_OPDS_SERVER_URL: "URL server OPDS"
|
||||
STR_SYNC_CLOCK: "Sync Clock"
|
||||
STR_TIME_SYNCED: "Time synced!"
|
||||
|
||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Отображение индексации"
|
||||
STR_INDEXING_POPUP: "Popup"
|
||||
STR_INDEXING_STATUS_TEXT: "Текст в строке"
|
||||
STR_INDEXING_STATUS_ICON: "Иконка в строке"
|
||||
STR_SYNC_CLOCK: "Sync Clock"
|
||||
STR_TIME_SYNCED: "Time synced!"
|
||||
|
||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Mostrar indexación"
|
||||
STR_INDEXING_POPUP: "Popup"
|
||||
STR_INDEXING_STATUS_TEXT: "Texto barra estado"
|
||||
STR_INDEXING_STATUS_ICON: "Icono barra estado"
|
||||
STR_SYNC_CLOCK: "Sync Clock"
|
||||
STR_TIME_SYNCED: "Time synced!"
|
||||
|
||||
@@ -341,3 +341,5 @@ STR_INDEXING_DISPLAY: "Indexeringsvisning"
|
||||
STR_INDEXING_POPUP: "Popup"
|
||||
STR_INDEXING_STATUS_TEXT: "Statusfältstext"
|
||||
STR_INDEXING_STATUS_ICON: "Statusfältsikon"
|
||||
STR_SYNC_CLOCK: "Sync Clock"
|
||||
STR_TIME_SYNCED: "Time synced!"
|
||||
|
||||
@@ -38,6 +38,8 @@ bool HalStorage::remove(const char* path) { return SDCard.remove(path); }
|
||||
|
||||
bool HalStorage::rmdir(const char* path) { return SDCard.rmdir(path); }
|
||||
|
||||
bool HalStorage::rename(const char* path, const char* newPath) { return SDCard.rename(path, newPath); }
|
||||
|
||||
bool HalStorage::openFileForRead(const char* moduleName, const char* path, FsFile& file) {
|
||||
return SDCard.openFileForRead(moduleName, path, file);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ class HalStorage {
|
||||
bool exists(const char* path);
|
||||
bool remove(const char* path);
|
||||
bool rmdir(const char* path);
|
||||
bool rename(const char* path, const char* newPath);
|
||||
|
||||
bool openFileForRead(const char* moduleName, const char* path, FsFile& file);
|
||||
bool openFileForRead(const char* moduleName, const std::string& path, FsFile& file);
|
||||
|
||||
@@ -47,6 +47,11 @@ void RecentBooksStore::removeBook(const std::string& path) {
|
||||
}
|
||||
}
|
||||
|
||||
void RecentBooksStore::clear() {
|
||||
recentBooks.clear();
|
||||
saveToFile();
|
||||
}
|
||||
|
||||
void RecentBooksStore::updateBook(const std::string& path, const std::string& title, const std::string& author,
|
||||
const std::string& coverBmpPath) {
|
||||
auto it =
|
||||
|
||||
@@ -33,6 +33,9 @@ class RecentBooksStore {
|
||||
// Remove a book from the recent list by path
|
||||
void removeBook(const std::string& path);
|
||||
|
||||
// Clear all recent books
|
||||
void clear();
|
||||
|
||||
// Get the list of recent books (most recent first)
|
||||
const std::vector<RecentBook>& getBooks() const { return recentBooks; }
|
||||
|
||||
|
||||
115
src/activities/home/BookManageMenuActivity.cpp
Normal file
115
src/activities/home/BookManageMenuActivity.cpp
Normal file
@@ -0,0 +1,115 @@
|
||||
#include "BookManageMenuActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
void BookManageMenuActivity::buildMenuItems() {
|
||||
menuItems.clear();
|
||||
if (archived) {
|
||||
menuItems.push_back({Action::UNARCHIVE, StrId::STR_UNARCHIVE_BOOK});
|
||||
} else {
|
||||
menuItems.push_back({Action::ARCHIVE, StrId::STR_ARCHIVE_BOOK});
|
||||
}
|
||||
menuItems.push_back({Action::DELETE, StrId::STR_DELETE_BOOK});
|
||||
menuItems.push_back({Action::DELETE_CACHE, StrId::STR_DELETE_CACHE_ONLY});
|
||||
menuItems.push_back({Action::REINDEX, StrId::STR_REINDEX_BOOK});
|
||||
}
|
||||
|
||||
void BookManageMenuActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
selectedIndex = 0;
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void BookManageMenuActivity::onExit() { Activity::onExit(); }
|
||||
|
||||
void BookManageMenuActivity::loop() {
|
||||
// Long-press detection: REINDEX_FULL when long-pressing on the Reindex item
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= LONG_PRESS_MS) {
|
||||
if (!ignoreNextConfirmRelease && selectedIndex < static_cast<int>(menuItems.size()) &&
|
||||
menuItems[selectedIndex].action == Action::REINDEX) {
|
||||
ignoreNextConfirmRelease = true;
|
||||
auto cb = onAction;
|
||||
cb(Action::REINDEX_FULL);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
buttonNavigator.onNext([this] {
|
||||
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator.onPrevious([this] {
|
||||
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (ignoreNextConfirmRelease) {
|
||||
ignoreNextConfirmRelease = false;
|
||||
return;
|
||||
}
|
||||
if (selectedIndex < static_cast<int>(menuItems.size())) {
|
||||
auto cb = onAction;
|
||||
cb(menuItems[selectedIndex].action);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
auto cb = onCancel;
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void BookManageMenuActivity::render(Activity::RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
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;
|
||||
|
||||
// Popup border and background
|
||||
renderer.fillRect(popupX - 2, popupY - 2, popupW + 4, popupH + 4, true);
|
||||
renderer.fillRect(popupX, popupY, popupW, popupH, false);
|
||||
|
||||
// Title
|
||||
renderer.drawText(UI_12_FONT_ID, popupX + popupMargin, popupY + 8, tr(STR_MANAGE_BOOK), true, EpdFontFamily::BOLD);
|
||||
|
||||
// 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_CANCEL), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
61
src/activities/home/BookManageMenuActivity.h
Normal file
61
src/activities/home/BookManageMenuActivity.h
Normal file
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <I18n.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Activity.h"
|
||||
#include "util/ButtonNavigator.h"
|
||||
|
||||
class BookManageMenuActivity final : public Activity {
|
||||
public:
|
||||
enum class Action {
|
||||
ARCHIVE,
|
||||
UNARCHIVE,
|
||||
DELETE,
|
||||
DELETE_CACHE,
|
||||
REINDEX,
|
||||
REINDEX_FULL,
|
||||
};
|
||||
|
||||
explicit BookManageMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::string& bookPath, bool isArchived,
|
||||
const std::function<void(Action)>& onAction,
|
||||
const std::function<void()>& onCancel,
|
||||
bool initialSkipRelease = false)
|
||||
: Activity("BookManageMenu", renderer, mappedInput),
|
||||
bookPath(bookPath),
|
||||
archived(isArchived),
|
||||
ignoreNextConfirmRelease(initialSkipRelease),
|
||||
onAction(onAction),
|
||||
onCancel(onCancel) {
|
||||
buildMenuItems();
|
||||
}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(Activity::RenderLock&&) override;
|
||||
|
||||
private:
|
||||
struct MenuItem {
|
||||
Action action;
|
||||
StrId labelId;
|
||||
};
|
||||
|
||||
std::string bookPath;
|
||||
bool archived;
|
||||
std::vector<MenuItem> menuItems;
|
||||
int selectedIndex = 0;
|
||||
ButtonNavigator buttonNavigator;
|
||||
|
||||
bool ignoreNextConfirmRelease;
|
||||
static constexpr unsigned long LONG_PRESS_MS = 700;
|
||||
|
||||
const std::function<void(Action)> onAction;
|
||||
const std::function<void()> onCancel;
|
||||
|
||||
void buildMenuItems();
|
||||
};
|
||||
@@ -13,12 +13,14 @@
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "BookManageMenuActivity.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "util/BookManager.h"
|
||||
#include "util/StringUtils.h"
|
||||
|
||||
int HomeActivity::getMenuItemCount() const {
|
||||
@@ -124,7 +126,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
|
||||
}
|
||||
|
||||
void HomeActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
ActivityWithSubactivity::onEnter();
|
||||
|
||||
// Check if OPDS browser URL is configured
|
||||
hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0;
|
||||
@@ -139,7 +141,7 @@ void HomeActivity::onEnter() {
|
||||
}
|
||||
|
||||
void HomeActivity::onExit() {
|
||||
Activity::onExit();
|
||||
ActivityWithSubactivity::onExit();
|
||||
|
||||
// Free the stored cover buffer if any
|
||||
freeCoverBuffer();
|
||||
@@ -188,6 +190,11 @@ void HomeActivity::freeCoverBuffer() {
|
||||
}
|
||||
|
||||
void HomeActivity::loop() {
|
||||
if (subActivity) {
|
||||
subActivity->loop();
|
||||
return;
|
||||
}
|
||||
|
||||
const int menuCount = getMenuItemCount();
|
||||
|
||||
buttonNavigator.onNext([this, menuCount] {
|
||||
@@ -200,7 +207,32 @@ void HomeActivity::loop() {
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
// Long-press Confirm: manage menu for recent books, or browse archive for Browse Files
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= LONG_PRESS_MS &&
|
||||
!ignoreNextConfirmRelease) {
|
||||
if (selectorIndex < static_cast<int>(recentBooks.size())) {
|
||||
// Long-press on a recent book → manage menu
|
||||
ignoreNextConfirmRelease = true;
|
||||
openManageMenu(recentBooks[selectorIndex].path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Browse Files is selected
|
||||
const int menuSelectedIndex = selectorIndex - static_cast<int>(recentBooks.size());
|
||||
if (menuSelectedIndex == 0) {
|
||||
// Long-press on Browse Files → go to archive folder
|
||||
ignoreNextConfirmRelease = true;
|
||||
onMyLibraryOpenWithPath("/.archive", true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (ignoreNextConfirmRelease) {
|
||||
ignoreNextConfirmRelease = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate dynamic indices based on which options are available
|
||||
int idx = 0;
|
||||
int menuSelectedIndex = selectorIndex - static_cast<int>(recentBooks.size());
|
||||
@@ -210,7 +242,7 @@ void HomeActivity::loop() {
|
||||
const int fileTransferIdx = idx++;
|
||||
const int settingsIdx = idx;
|
||||
|
||||
if (selectorIndex < recentBooks.size()) {
|
||||
if (selectorIndex < static_cast<int>(recentBooks.size())) {
|
||||
onSelectBook(recentBooks[selectorIndex].path);
|
||||
} else if (menuSelectedIndex == myLibraryIdx) {
|
||||
onMyLibraryOpen();
|
||||
@@ -273,3 +305,53 @@ void HomeActivity::render(Activity::RenderLock&&) {
|
||||
loadRecentCovers(metrics.homeCoverHeight);
|
||||
}
|
||||
}
|
||||
|
||||
void HomeActivity::openManageMenu(const std::string& bookPath) {
|
||||
const bool isArchived = BookManager::isArchived(bookPath);
|
||||
const std::string capturedPath = bookPath;
|
||||
enterNewActivity(new BookManageMenuActivity(
|
||||
renderer, mappedInput, capturedPath, isArchived,
|
||||
[this, capturedPath](BookManageMenuActivity::Action action) {
|
||||
exitActivity();
|
||||
bool success = false;
|
||||
switch (action) {
|
||||
case BookManageMenuActivity::Action::ARCHIVE:
|
||||
success = BookManager::archiveBook(capturedPath);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::UNARCHIVE:
|
||||
success = BookManager::unarchiveBook(capturedPath);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::DELETE:
|
||||
success = BookManager::deleteBook(capturedPath);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::DELETE_CACHE:
|
||||
success = BookManager::deleteBookCache(capturedPath);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::REINDEX:
|
||||
success = BookManager::reindexBook(capturedPath, false);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::REINDEX_FULL:
|
||||
success = BookManager::reindexBook(capturedPath, true);
|
||||
break;
|
||||
}
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
GUI.drawPopup(renderer, success ? tr(STR_DONE) : tr(STR_ACTION_FAILED));
|
||||
}
|
||||
requestUpdateAndWait();
|
||||
// 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));
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
#include "../Activity.h"
|
||||
#include "../ActivityWithSubactivity.h"
|
||||
#include "./MyLibraryActivity.h"
|
||||
#include "util/ButtonNavigator.h"
|
||||
|
||||
struct RecentBook;
|
||||
struct Rect;
|
||||
|
||||
class HomeActivity final : public Activity {
|
||||
class HomeActivity final : public ActivityWithSubactivity {
|
||||
ButtonNavigator buttonNavigator;
|
||||
int selectorIndex = 0;
|
||||
bool recentsLoading = false;
|
||||
@@ -20,8 +20,14 @@ 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<RecentBook> recentBooks;
|
||||
|
||||
// Long-press state
|
||||
bool ignoreNextConfirmRelease = false;
|
||||
static constexpr unsigned long LONG_PRESS_MS = 700;
|
||||
|
||||
const std::function<void(const std::string& path)> onSelectBook;
|
||||
const std::function<void()> onMyLibraryOpen;
|
||||
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;
|
||||
@@ -33,16 +39,20 @@ class HomeActivity final : public Activity {
|
||||
void freeCoverBuffer(); // Free the stored cover buffer
|
||||
void loadRecentBooks(int maxBooks);
|
||||
void loadRecentCovers(int coverHeight);
|
||||
void openManageMenu(const std::string& bookPath);
|
||||
|
||||
public:
|
||||
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void(const std::string& path)>& onSelectBook,
|
||||
const std::function<void()>& onMyLibraryOpen, const std::function<void()>& onRecentsOpen,
|
||||
const std::function<void()>& onMyLibraryOpen,
|
||||
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)
|
||||
: Activity("Home", renderer, mappedInput),
|
||||
: ActivityWithSubactivity("Home", renderer, mappedInput),
|
||||
onSelectBook(onSelectBook),
|
||||
onMyLibraryOpen(onMyLibraryOpen),
|
||||
onMyLibraryOpenWithPath(onMyLibraryOpenWithPath),
|
||||
onRecentsOpen(onRecentsOpen),
|
||||
onSettingsOpen(onSettingsOpen),
|
||||
onFileTransferOpen(onFileTransferOpen),
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "BookManageMenuActivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "util/BookManager.h"
|
||||
#include "util/StringUtils.h"
|
||||
|
||||
namespace {
|
||||
@@ -103,7 +105,7 @@ void MyLibraryActivity::loadFiles() {
|
||||
}
|
||||
|
||||
void MyLibraryActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
ActivityWithSubactivity::onEnter();
|
||||
|
||||
loadFiles();
|
||||
selectorIndex = 0;
|
||||
@@ -112,11 +114,26 @@ void MyLibraryActivity::onEnter() {
|
||||
}
|
||||
|
||||
void MyLibraryActivity::onExit() {
|
||||
Activity::onExit();
|
||||
ActivityWithSubactivity::onExit();
|
||||
files.clear();
|
||||
}
|
||||
|
||||
void MyLibraryActivity::loop() {
|
||||
if (subActivity) {
|
||||
subActivity->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 != "/") {
|
||||
@@ -128,7 +145,28 @@ void MyLibraryActivity::loop() {
|
||||
|
||||
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false);
|
||||
|
||||
// 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 && isBookFile) {
|
||||
ignoreNextConfirmRelease = true;
|
||||
const std::string fullPath = (basepath.back() == '/' ? basepath : basepath + "/") + files[selectorIndex];
|
||||
if (inArchive) {
|
||||
unarchiveAndOpen(fullPath);
|
||||
} else {
|
||||
openManageMenu(fullPath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (ignoreNextConfirmRelease) {
|
||||
ignoreNextConfirmRelease = false;
|
||||
return;
|
||||
}
|
||||
if (files.empty()) {
|
||||
return;
|
||||
}
|
||||
@@ -139,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;
|
||||
@@ -235,6 +276,94 @@ void MyLibraryActivity::render(Activity::RenderLock&&) {
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
|
||||
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,
|
||||
[this, capturedPath](BookManageMenuActivity::Action action) {
|
||||
exitActivity();
|
||||
bool success = false;
|
||||
switch (action) {
|
||||
case BookManageMenuActivity::Action::ARCHIVE:
|
||||
success = BookManager::archiveBook(capturedPath);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::UNARCHIVE:
|
||||
success = BookManager::unarchiveBook(capturedPath);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::DELETE:
|
||||
success = BookManager::deleteBook(capturedPath);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::DELETE_CACHE:
|
||||
success = BookManager::deleteBookCache(capturedPath);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::REINDEX:
|
||||
success = BookManager::reindexBook(capturedPath, false);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::REINDEX_FULL:
|
||||
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 (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();
|
||||
},
|
||||
[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 {
|
||||
for (size_t i = 0; i < files.size(); i++)
|
||||
if (files[i] == name) return i;
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Activity.h"
|
||||
#include "../ActivityWithSubactivity.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "util/ButtonNavigator.h"
|
||||
|
||||
class MyLibraryActivity final : public Activity {
|
||||
class MyLibraryActivity final : public ActivityWithSubactivity {
|
||||
private:
|
||||
ButtonNavigator buttonNavigator;
|
||||
|
||||
@@ -17,6 +17,13 @@ class MyLibraryActivity final : public Activity {
|
||||
std::string basepath = "/";
|
||||
std::vector<std::string> files;
|
||||
|
||||
// Long-press state
|
||||
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;
|
||||
@@ -24,14 +31,19 @@ class MyLibraryActivity final : public Activity {
|
||||
// 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 = "/")
|
||||
: Activity("MyLibrary", renderer, mappedInput),
|
||||
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;
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "BookManageMenuActivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "util/BookManager.h"
|
||||
#include "util/StringUtils.h"
|
||||
|
||||
namespace {
|
||||
@@ -31,7 +33,7 @@ void RecentBooksActivity::loadRecentBooks() {
|
||||
}
|
||||
|
||||
void RecentBooksActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
ActivityWithSubactivity::onEnter();
|
||||
|
||||
// Load data
|
||||
loadRecentBooks();
|
||||
@@ -41,14 +43,33 @@ void RecentBooksActivity::onEnter() {
|
||||
}
|
||||
|
||||
void RecentBooksActivity::onExit() {
|
||||
Activity::onExit();
|
||||
ActivityWithSubactivity::onExit();
|
||||
recentBooks.clear();
|
||||
}
|
||||
|
||||
void RecentBooksActivity::loop() {
|
||||
if (subActivity) {
|
||||
subActivity->loop();
|
||||
return;
|
||||
}
|
||||
|
||||
const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, true);
|
||||
|
||||
// Long-press Confirm: open manage menu
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && mappedInput.getHeldTime() >= LONG_PRESS_MS &&
|
||||
!ignoreNextConfirmRelease) {
|
||||
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
|
||||
ignoreNextConfirmRelease = true;
|
||||
openManageMenu(recentBooks[selectorIndex].path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (ignoreNextConfirmRelease) {
|
||||
ignoreNextConfirmRelease = false;
|
||||
return;
|
||||
}
|
||||
if (!recentBooks.empty() && selectorIndex < static_cast<int>(recentBooks.size())) {
|
||||
LOG_DBG("RBA", "Selected recent book: %s", recentBooks[selectorIndex].path.c_str());
|
||||
onSelectBook(recentBooks[selectorIndex].path);
|
||||
@@ -111,3 +132,49 @@ void RecentBooksActivity::render(Activity::RenderLock&&) {
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
|
||||
void RecentBooksActivity::openManageMenu(const std::string& bookPath) {
|
||||
const bool isArchived = BookManager::isArchived(bookPath);
|
||||
const std::string capturedPath = bookPath;
|
||||
enterNewActivity(new BookManageMenuActivity(
|
||||
renderer, mappedInput, capturedPath, isArchived,
|
||||
[this, capturedPath](BookManageMenuActivity::Action action) {
|
||||
exitActivity();
|
||||
bool success = false;
|
||||
switch (action) {
|
||||
case BookManageMenuActivity::Action::ARCHIVE:
|
||||
success = BookManager::archiveBook(capturedPath);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::UNARCHIVE:
|
||||
success = BookManager::unarchiveBook(capturedPath);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::DELETE:
|
||||
success = BookManager::deleteBook(capturedPath);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::DELETE_CACHE:
|
||||
success = BookManager::deleteBookCache(capturedPath);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::REINDEX:
|
||||
success = BookManager::reindexBook(capturedPath, false);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::REINDEX_FULL:
|
||||
success = BookManager::reindexBook(capturedPath, true);
|
||||
break;
|
||||
}
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
GUI.drawPopup(renderer, success ? tr(STR_DONE) : tr(STR_ACTION_FAILED));
|
||||
}
|
||||
requestUpdateAndWait();
|
||||
loadRecentBooks();
|
||||
if (selectorIndex >= static_cast<int>(recentBooks.size()) && !recentBooks.empty()) {
|
||||
selectorIndex = recentBooks.size() - 1;
|
||||
}
|
||||
requestUpdate();
|
||||
},
|
||||
[this] {
|
||||
exitActivity();
|
||||
requestUpdate();
|
||||
},
|
||||
true));
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Activity.h"
|
||||
#include "../ActivityWithSubactivity.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "util/ButtonNavigator.h"
|
||||
|
||||
class RecentBooksActivity final : public Activity {
|
||||
class RecentBooksActivity final : public ActivityWithSubactivity {
|
||||
private:
|
||||
ButtonNavigator buttonNavigator;
|
||||
|
||||
@@ -18,18 +18,23 @@ class RecentBooksActivity final : public Activity {
|
||||
// Recent tab state
|
||||
std::vector<RecentBook> recentBooks;
|
||||
|
||||
// Long-press state
|
||||
bool ignoreNextConfirmRelease = false;
|
||||
static constexpr unsigned long LONG_PRESS_MS = 700;
|
||||
|
||||
// Callbacks
|
||||
const std::function<void(const std::string& path)> onSelectBook;
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
// Data loading
|
||||
void loadRecentBooks();
|
||||
void openManageMenu(const std::string& bookPath);
|
||||
|
||||
public:
|
||||
explicit RecentBooksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void()>& onGoHome,
|
||||
const std::function<void(const std::string& path)>& onSelectBook)
|
||||
: Activity("RecentBooks", renderer, mappedInput), onSelectBook(onSelectBook), onGoHome(onGoHome) {}
|
||||
: ActivityWithSubactivity("RecentBooks", renderer, mappedInput), onSelectBook(onSelectBook), onGoHome(onGoHome) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
|
||||
74
src/activities/reader/EndOfBookMenuActivity.cpp
Normal file
74
src/activities/reader/EndOfBookMenuActivity.cpp
Normal file
@@ -0,0 +1,74 @@
|
||||
#include "EndOfBookMenuActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
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});
|
||||
}
|
||||
|
||||
void EndOfBookMenuActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
selectedIndex = 0;
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void EndOfBookMenuActivity::onExit() { Activity::onExit(); }
|
||||
|
||||
void EndOfBookMenuActivity::loop() {
|
||||
buttonNavigator.onNext([this] {
|
||||
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator.onPrevious([this] {
|
||||
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast<int>(menuItems.size()));
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (selectedIndex < static_cast<int>(menuItems.size())) {
|
||||
auto cb = onAction;
|
||||
cb(menuItems[selectedIndex].action);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
auto cb = onAction;
|
||||
cb(Action::CLOSE_MENU);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void EndOfBookMenuActivity::render(Activity::RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
auto metrics = UITheme::getInstance().getMetrics();
|
||||
|
||||
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_END_OF_BOOK));
|
||||
|
||||
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
|
||||
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing;
|
||||
|
||||
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)); });
|
||||
|
||||
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();
|
||||
}
|
||||
47
src/activities/reader/EndOfBookMenuActivity.h
Normal file
47
src/activities/reader/EndOfBookMenuActivity.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <I18n.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../Activity.h"
|
||||
#include "util/ButtonNavigator.h"
|
||||
|
||||
class EndOfBookMenuActivity final : public Activity {
|
||||
public:
|
||||
enum class Action {
|
||||
ARCHIVE,
|
||||
DELETE,
|
||||
TABLE_OF_CONTENTS,
|
||||
BACK_TO_BEGINNING,
|
||||
CLOSE_BOOK,
|
||||
CLOSE_MENU,
|
||||
};
|
||||
|
||||
explicit EndOfBookMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& bookPath,
|
||||
const std::function<void(Action)>& onAction)
|
||||
: Activity("EndOfBookMenu", renderer, mappedInput), bookPath(bookPath), onAction(onAction) {
|
||||
buildMenuItems();
|
||||
}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(Activity::RenderLock&&) override;
|
||||
|
||||
private:
|
||||
struct MenuItem {
|
||||
Action action;
|
||||
StrId labelId;
|
||||
};
|
||||
|
||||
std::string bookPath;
|
||||
std::vector<MenuItem> menuItems;
|
||||
int selectedIndex = 0;
|
||||
ButtonNavigator buttonNavigator;
|
||||
const std::function<void(Action)> onAction;
|
||||
|
||||
void buildMenuItems();
|
||||
};
|
||||
@@ -13,12 +13,14 @@
|
||||
#include "EpubReaderBookmarkSelectionActivity.h"
|
||||
#include "EpubReaderChapterSelectionActivity.h"
|
||||
#include "EpubReaderPercentSelectionActivity.h"
|
||||
#include "EndOfBookMenuActivity.h"
|
||||
#include "KOReaderCredentialStore.h"
|
||||
#include "KOReaderSyncActivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "util/BookManager.h"
|
||||
#include "util/BookmarkStore.h"
|
||||
#include "util/Dictionary.h"
|
||||
|
||||
@@ -220,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
|
||||
@@ -264,7 +312,7 @@ void EpubReaderActivity::loop() {
|
||||
exitActivity();
|
||||
enterNewActivity(new EpubReaderMenuActivity(
|
||||
this->renderer, this->mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
||||
SETTINGS.orientation, SETTINGS.fontSize, hasDictionary, isBookmarked, epub->getCachePath(),
|
||||
SETTINGS.orientation, SETTINGS.fontSize, hasDictionary, isBookmarked, epub->getCachePath(), epub->getPath(),
|
||||
[this](const uint8_t orientation, const uint8_t fontSize) { onReaderMenuBack(orientation, fontSize); },
|
||||
[this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); }));
|
||||
}
|
||||
@@ -299,10 +347,11 @@ void EpubReaderActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// any botton press when at end of the book goes back to the last page
|
||||
// any button press when at end of the book goes back to the last page
|
||||
if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) {
|
||||
currentSpineIndex = epub->getSpineItemsCount() - 1;
|
||||
nextPageNumber = UINT16_MAX;
|
||||
endOfBookMenuOpened = false;
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
@@ -712,6 +761,36 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
||||
pendingGoHome = true;
|
||||
break;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::ARCHIVE_BOOK: {
|
||||
if (epub) {
|
||||
BookManager::archiveBook(epub->getPath());
|
||||
}
|
||||
pendingGoHome = true;
|
||||
break;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::DELETE_BOOK: {
|
||||
if (epub) {
|
||||
BookManager::deleteBook(epub->getPath());
|
||||
}
|
||||
pendingGoHome = true;
|
||||
break;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::MANAGE_BOOK:
|
||||
break;
|
||||
case EpubReaderMenuActivity::MenuAction::REINDEX_BOOK: {
|
||||
if (epub) {
|
||||
BookManager::reindexBook(epub->getPath(), false);
|
||||
}
|
||||
pendingGoHome = true;
|
||||
break;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::REINDEX_BOOK_FULL: {
|
||||
if (epub) {
|
||||
BookManager::reindexBook(epub->getPath(), true);
|
||||
}
|
||||
pendingGoHome = true;
|
||||
break;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::SYNC: {
|
||||
if (KOREADER_STORE.hasCredentials()) {
|
||||
const int currentPage = section ? section->currentPage : 0;
|
||||
@@ -807,11 +886,11 @@ 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()) {
|
||||
renderer.clearScreen();
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
|
||||
renderer.displayBuffer();
|
||||
if (!endOfBookMenuOpened) {
|
||||
pendingEndOfBookMenu = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
|
||||
volatile bool loadingSection = false; // True during the entire !section block (read from main loop)
|
||||
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;
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include "../home/BookManageMenuActivity.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "util/BookManager.h"
|
||||
|
||||
void EpubReaderMenuActivity::onEnter() {
|
||||
ActivityWithSubactivity::onEnter();
|
||||
@@ -116,6 +118,42 @@ void EpubReaderMenuActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedAction == MenuAction::MANAGE_BOOK) {
|
||||
const bool isArchived = BookManager::isArchived(bookFilePath);
|
||||
enterNewActivity(new BookManageMenuActivity(
|
||||
renderer, mappedInput, bookFilePath, isArchived,
|
||||
[this](BookManageMenuActivity::Action action) {
|
||||
exitActivity();
|
||||
auto cb = onAction;
|
||||
switch (action) {
|
||||
case BookManageMenuActivity::Action::ARCHIVE:
|
||||
cb(MenuAction::ARCHIVE_BOOK);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::DELETE:
|
||||
cb(MenuAction::DELETE_BOOK);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::DELETE_CACHE:
|
||||
cb(MenuAction::DELETE_CACHE);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::REINDEX:
|
||||
cb(MenuAction::REINDEX_BOOK);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::REINDEX_FULL:
|
||||
cb(MenuAction::REINDEX_BOOK_FULL);
|
||||
break;
|
||||
case BookManageMenuActivity::Action::UNARCHIVE:
|
||||
// Unarchive from within reader is unusual but handle gracefully
|
||||
cb(MenuAction::GO_HOME);
|
||||
break;
|
||||
}
|
||||
},
|
||||
[this] {
|
||||
exitActivity();
|
||||
requestUpdate();
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Capture the callback and action locally
|
||||
auto actionCallback = onAction;
|
||||
|
||||
|
||||
@@ -28,12 +28,18 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
||||
GO_HOME,
|
||||
SYNC,
|
||||
DELETE_CACHE,
|
||||
MANAGE_BOOK,
|
||||
ARCHIVE_BOOK,
|
||||
DELETE_BOOK,
|
||||
REINDEX_BOOK,
|
||||
REINDEX_BOOK_FULL,
|
||||
};
|
||||
|
||||
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
|
||||
const int currentPage, const int totalPages, const int bookProgressPercent,
|
||||
const uint8_t currentOrientation, const uint8_t currentFontSize,
|
||||
const bool hasDictionary, const bool isBookmarked, const std::string& bookCachePath,
|
||||
const std::string& bookFilePath,
|
||||
const std::function<void(uint8_t, uint8_t)>& onBack,
|
||||
const std::function<void(MenuAction)>& onAction)
|
||||
: ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput),
|
||||
@@ -42,6 +48,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
||||
pendingOrientation(currentOrientation),
|
||||
pendingFontSize(currentFontSize),
|
||||
bookCachePath(bookCachePath),
|
||||
bookFilePath(bookFilePath),
|
||||
currentPage(currentPage),
|
||||
totalPages(totalPages),
|
||||
bookProgressPercent(bookProgressPercent),
|
||||
@@ -75,6 +82,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
||||
StrId::STR_LANDSCAPE_CCW};
|
||||
const std::vector<StrId> fontSizeLabels = {StrId::STR_SMALL, StrId::STR_MEDIUM, StrId::STR_LARGE, StrId::STR_X_LARGE};
|
||||
std::string bookCachePath;
|
||||
std::string bookFilePath;
|
||||
// Letterbox fill override: 0xFF = Default (use global), 0 = Dithered, 1 = Solid, 2 = None
|
||||
uint8_t pendingLetterboxFill = BookSettings::USE_GLOBAL;
|
||||
static constexpr int LETTERBOX_FILL_OPTION_COUNT = 4; // Default + 3 modes
|
||||
@@ -132,7 +140,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
||||
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
|
||||
items.push_back({MenuAction::GO_HOME, StrId::STR_CLOSE_BOOK});
|
||||
items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS});
|
||||
items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE});
|
||||
items.push_back({MenuAction::MANAGE_BOOK, StrId::STR_MANAGE_BOOK});
|
||||
return items;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "EndOfBookMenuActivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "util/BookManager.h"
|
||||
|
||||
namespace {
|
||||
constexpr unsigned long goHomeMs = 1000;
|
||||
@@ -153,10 +155,45 @@ void TxtReaderActivity::loop() {
|
||||
|
||||
if (prevTriggered && currentPage > 0) {
|
||||
currentPage--;
|
||||
endOfBookMenuOpened = false;
|
||||
requestUpdate();
|
||||
} else if (nextTriggered && currentPage < totalPages - 1) {
|
||||
currentPage++;
|
||||
requestUpdate();
|
||||
} else if (nextTriggered && currentPage == totalPages - 1 && !endOfBookMenuOpened) {
|
||||
// At last page and trying to advance → show end of book menu
|
||||
endOfBookMenuOpened = true;
|
||||
const std::string path = txt->getPath();
|
||||
enterNewActivity(new EndOfBookMenuActivity(
|
||||
renderer, mappedInput, path, [this](EndOfBookMenuActivity::Action action) {
|
||||
exitActivity();
|
||||
switch (action) {
|
||||
case EndOfBookMenuActivity::Action::ARCHIVE:
|
||||
if (txt) BookManager::archiveBook(txt->getPath());
|
||||
if (onGoHome) onGoHome();
|
||||
return;
|
||||
case EndOfBookMenuActivity::Action::DELETE:
|
||||
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;
|
||||
requestUpdate();
|
||||
break;
|
||||
case EndOfBookMenuActivity::Action::CLOSE_BOOK:
|
||||
if (onGoHome) onGoHome();
|
||||
return;
|
||||
case EndOfBookMenuActivity::Action::CLOSE_MENU:
|
||||
endOfBookMenuOpened = false;
|
||||
requestUpdate();
|
||||
break;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ class TxtReaderActivity final : public ActivityWithSubactivity {
|
||||
int totalPages = 1;
|
||||
int pagesUntilFullRefresh = 0;
|
||||
|
||||
bool endOfBookMenuOpened = false;
|
||||
const std::function<void()> onGoBack;
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "CrossPointState.h"
|
||||
#include "EndOfBookMenuActivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "XtcReaderChapterSelectionActivity.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "util/BookManager.h"
|
||||
|
||||
namespace {
|
||||
constexpr unsigned long skipPageMs = 700;
|
||||
@@ -104,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()) {
|
||||
@@ -155,6 +211,7 @@ void XtcReaderActivity::loop() {
|
||||
// Handle end of book
|
||||
if (currentPage >= xtc->getPageCount()) {
|
||||
currentPage = xtc->getPageCount() - 1;
|
||||
endOfBookMenuOpened = false;
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
@@ -183,12 +240,11 @@ void XtcReaderActivity::render(Activity::RenderLock&&) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bounds check
|
||||
// End of book — defer menu creation to loop() to avoid deadlock (render holds the lock)
|
||||
if (currentPage >= xtc->getPageCount()) {
|
||||
// Show end of book screen
|
||||
renderer.clearScreen();
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
|
||||
renderer.displayBuffer();
|
||||
if (!endOfBookMenuOpened) {
|
||||
pendingEndOfBookMenu = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ class XtcReaderActivity final : public ActivityWithSubactivity {
|
||||
uint32_t currentPage = 0;
|
||||
int pagesUntilFullRefresh = 0;
|
||||
|
||||
bool endOfBookMenuOpened = false;
|
||||
bool pendingEndOfBookMenu = false;
|
||||
const std::function<void()> onGoBack;
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <Logging.h>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
@@ -94,8 +95,8 @@ void ClearCacheActivity::clearCache() {
|
||||
file.getName(name, sizeof(name));
|
||||
String itemName(name);
|
||||
|
||||
// Only delete directories starting with epub_ or xtc_
|
||||
if (file.isDirectory() && (itemName.startsWith("epub_") || itemName.startsWith("xtc_"))) {
|
||||
if (file.isDirectory() &&
|
||||
(itemName.startsWith("epub_") || itemName.startsWith("xtc_") || itemName.startsWith("txt_"))) {
|
||||
String fullPath = "/.crosspoint/" + itemName;
|
||||
LOG_DBG("CLEAR_CACHE", "Removing cache: %s", fullPath.c_str());
|
||||
|
||||
@@ -113,6 +114,9 @@ void ClearCacheActivity::clearCache() {
|
||||
}
|
||||
root.close();
|
||||
|
||||
// Clear recents since all cached data (covers, progress) is gone
|
||||
RECENT_BOOKS.clear();
|
||||
|
||||
LOG_DBG("CLEAR_CACHE", "Cache cleared: %d removed, %d failed", clearedCount, failedCount);
|
||||
|
||||
state = SUCCESS;
|
||||
|
||||
150
src/activities/settings/NtpSyncActivity.cpp
Normal file
150
src/activities/settings/NtpSyncActivity.cpp
Normal file
@@ -0,0 +1,150 @@
|
||||
#include "NtpSyncActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
#include <Logging.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "activities/network/WifiSelectionActivity.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "util/TimeSync.h"
|
||||
|
||||
static constexpr unsigned long AUTO_DISMISS_MS = 5000;
|
||||
|
||||
void NtpSyncActivity::onWifiSelectionComplete(const bool success) {
|
||||
exitActivity();
|
||||
|
||||
if (!success) {
|
||||
LOG_ERR("NTP", "WiFi connection failed, exiting");
|
||||
goBack();
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DBG("NTP", "WiFi connected, starting NTP sync");
|
||||
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
state = SYNCING;
|
||||
}
|
||||
requestUpdateAndWait();
|
||||
|
||||
const bool synced = TimeSync::waitForNtpSync(8000);
|
||||
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
state = synced ? SUCCESS : FAILED;
|
||||
if (synced) {
|
||||
successTimestamp = millis();
|
||||
}
|
||||
}
|
||||
requestUpdate();
|
||||
|
||||
if (synced) {
|
||||
LOG_DBG("NTP", "Time synced successfully");
|
||||
} else {
|
||||
LOG_ERR("NTP", "NTP sync timed out");
|
||||
}
|
||||
}
|
||||
|
||||
void NtpSyncActivity::onEnter() {
|
||||
ActivityWithSubactivity::onEnter();
|
||||
|
||||
LOG_DBG("NTP", "Turning on WiFi...");
|
||||
WiFi.mode(WIFI_STA);
|
||||
|
||||
LOG_DBG("NTP", "Launching WifiSelectionActivity...");
|
||||
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
||||
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
||||
}
|
||||
|
||||
void NtpSyncActivity::onExit() {
|
||||
ActivityWithSubactivity::onExit();
|
||||
|
||||
TimeSync::stopNtpSync();
|
||||
WiFi.disconnect(false);
|
||||
delay(100);
|
||||
WiFi.mode(WIFI_OFF);
|
||||
delay(100);
|
||||
}
|
||||
|
||||
void NtpSyncActivity::render(Activity::RenderLock&&) {
|
||||
if (subActivity) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto metrics = UITheme::getInstance().getMetrics();
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
renderer.clearScreen();
|
||||
|
||||
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_SYNC_CLOCK));
|
||||
|
||||
const auto lineHeight = renderer.getLineHeight(UI_10_FONT_ID);
|
||||
const auto centerY = (pageHeight - lineHeight) / 2;
|
||||
|
||||
if (state == SYNCING) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_SYNCING_TIME));
|
||||
} else if (state == SUCCESS) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_TIME_SYNCED), true, EpdFontFamily::BOLD);
|
||||
|
||||
time_t now = time(nullptr);
|
||||
struct tm* t = localtime(&now);
|
||||
if (t != nullptr && t->tm_year > 100) {
|
||||
char timeBuf[32];
|
||||
if (SETTINGS.clockFormat == CrossPointSettings::CLOCK_24H) {
|
||||
snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", t->tm_hour, t->tm_min);
|
||||
} else {
|
||||
int hour12 = t->tm_hour % 12;
|
||||
if (hour12 == 0) hour12 = 12;
|
||||
snprintf(timeBuf, sizeof(timeBuf), "%d:%02d %s", hour12, t->tm_min, t->tm_hour >= 12 ? "PM" : "AM");
|
||||
}
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY + lineHeight + metrics.verticalSpacing, timeBuf);
|
||||
}
|
||||
|
||||
const unsigned long elapsed = millis() - successTimestamp;
|
||||
const int remaining = static_cast<int>((AUTO_DISMISS_MS - elapsed + 999) / 1000);
|
||||
char backLabel[32];
|
||||
snprintf(backLabel, sizeof(backLabel), "%s (%d)", tr(STR_BACK), remaining > 0 ? remaining : 1);
|
||||
const auto labels = mappedInput.mapLabels(backLabel, "", "", "");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
} else if (state == FAILED) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_SYNC_FAILED_MSG), true, EpdFontFamily::BOLD);
|
||||
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
}
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
|
||||
void NtpSyncActivity::loop() {
|
||||
if (subActivity) {
|
||||
subActivity->loop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == SUCCESS) {
|
||||
const unsigned long elapsed = millis() - successTimestamp;
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back) || elapsed >= AUTO_DISMISS_MS) {
|
||||
goBack();
|
||||
return;
|
||||
}
|
||||
const int currentSecond = static_cast<int>(elapsed / 1000);
|
||||
if (currentSecond != lastCountdownSecond) {
|
||||
lastCountdownSecond = currentSecond;
|
||||
requestUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == FAILED) {
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
goBack();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
24
src/activities/settings/NtpSyncActivity.h
Normal file
24
src/activities/settings/NtpSyncActivity.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "activities/ActivityWithSubactivity.h"
|
||||
|
||||
class NtpSyncActivity : public ActivityWithSubactivity {
|
||||
enum State { WIFI_SELECTION, SYNCING, SUCCESS, FAILED };
|
||||
|
||||
const std::function<void()> goBack;
|
||||
State state = WIFI_SELECTION;
|
||||
unsigned long successTimestamp = 0;
|
||||
int lastCountdownSecond = -1;
|
||||
|
||||
void onWifiSelectionComplete(bool success);
|
||||
|
||||
public:
|
||||
explicit NtpSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void()>& goBack)
|
||||
: ActivityWithSubactivity("NtpSync", renderer, mappedInput), goBack(goBack) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(Activity::RenderLock&&) override;
|
||||
bool preventAutoSleep() override { return state == SYNCING; }
|
||||
};
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "KOReaderSettingsActivity.h"
|
||||
#include "LanguageSelectActivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "NtpSyncActivity.h"
|
||||
#include "OtaUpdateActivity.h"
|
||||
#include "SetTimeActivity.h"
|
||||
#include "SetTimezoneOffsetActivity.h"
|
||||
@@ -221,6 +222,9 @@ void SettingsActivity::toggleCurrentSetting() {
|
||||
case SettingAction::SetTimezoneOffset:
|
||||
enterSubActivity(new SetTimezoneOffsetActivity(renderer, mappedInput, onComplete));
|
||||
break;
|
||||
case SettingAction::SyncClock:
|
||||
enterSubActivity(new NtpSyncActivity(renderer, mappedInput, onComplete));
|
||||
break;
|
||||
case SettingAction::None:
|
||||
// Do nothing
|
||||
break;
|
||||
@@ -245,7 +249,8 @@ void SettingsActivity::rebuildClockActions() {
|
||||
[](const SettingInfo& s) { return s.type == SettingType::ACTION; }),
|
||||
clockSettings.end());
|
||||
|
||||
// Always add Set Time
|
||||
// Always add Sync Clock and Set Time
|
||||
clockSettings.push_back(SettingInfo::Action(StrId::STR_SYNC_CLOCK, SettingAction::SyncClock));
|
||||
clockSettings.push_back(SettingInfo::Action(StrId::STR_SET_TIME, SettingAction::SetTime));
|
||||
|
||||
// Only add Set UTC Offset when timezone is set to Custom
|
||||
|
||||
@@ -23,6 +23,7 @@ enum class SettingAction {
|
||||
Language,
|
||||
SetTime,
|
||||
SetTimezoneOffset,
|
||||
SyncClock,
|
||||
};
|
||||
|
||||
struct SettingInfo {
|
||||
|
||||
14
src/main.cpp
14
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() {
|
||||
@@ -264,8 +265,9 @@ void onGoToBrowser() {
|
||||
|
||||
void onGoHome() {
|
||||
exitActivity();
|
||||
enterNewActivity(new HomeActivity(renderer, mappedInputManager, onGoToReader, onGoToMyLibrary, onGoToRecentBooks,
|
||||
onGoToSettings, onGoToFileTransfer, onGoToBrowser));
|
||||
enterNewActivity(new HomeActivity(renderer, mappedInputManager, onGoToReader, onGoToMyLibrary,
|
||||
onGoToMyLibraryWithPath, onGoToRecentBooks, onGoToSettings, onGoToFileTransfer,
|
||||
onGoToBrowser));
|
||||
}
|
||||
|
||||
void setupDisplayAndFonts() {
|
||||
|
||||
255
src/util/BookManager.cpp
Normal file
255
src/util/BookManager.cpp
Normal file
@@ -0,0 +1,255 @@
|
||||
#include "BookManager.h"
|
||||
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "RecentBooksStore.h"
|
||||
#include "StringUtils.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char ARCHIVE_ROOT[] = "/.archive";
|
||||
constexpr char CACHE_ROOT[] = "/.crosspoint";
|
||||
|
||||
std::string getCachePrefix(const std::string& bookPath) {
|
||||
if (StringUtils::checkFileExtension(bookPath, ".epub")) return "epub_";
|
||||
if (StringUtils::checkFileExtension(bookPath, ".xtc") || StringUtils::checkFileExtension(bookPath, ".xtch"))
|
||||
return "xtc_";
|
||||
if (StringUtils::checkFileExtension(bookPath, ".txt") || StringUtils::checkFileExtension(bookPath, ".md"))
|
||||
return "txt_";
|
||||
return "epub_";
|
||||
}
|
||||
|
||||
std::string computeCachePath(const std::string& bookPath) {
|
||||
const auto prefix = getCachePrefix(bookPath);
|
||||
return std::string(CACHE_ROOT) + "/" + prefix + std::to_string(std::hash<std::string>{}(bookPath));
|
||||
}
|
||||
|
||||
// Ensure all parent directories of a path exist.
|
||||
void ensureParentDirs(const std::string& path) {
|
||||
for (size_t i = 1; i < path.length(); i++) {
|
||||
if (path[i] == '/') {
|
||||
Storage.mkdir(path.substr(0, i).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete cover and thumbnail BMP files from a cache directory.
|
||||
void deleteCoverFiles(const std::string& cachePath) {
|
||||
auto dir = Storage.open(cachePath.c_str());
|
||||
if (!dir || !dir.isDirectory()) {
|
||||
if (dir) dir.close();
|
||||
return;
|
||||
}
|
||||
dir.rewindDirectory();
|
||||
|
||||
char name[256];
|
||||
for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) {
|
||||
file.getName(name, sizeof(name));
|
||||
const std::string fname(name);
|
||||
file.close();
|
||||
|
||||
const bool isCover = (fname == "cover.bmp" || fname == "cover_crop.bmp");
|
||||
const bool isThumb = (fname.rfind("thumb_", 0) == 0 && StringUtils::checkFileExtension(fname, ".bmp"));
|
||||
|
||||
if (isCover || isThumb) {
|
||||
const std::string fullPath = cachePath + "/" + fname;
|
||||
Storage.remove(fullPath.c_str());
|
||||
}
|
||||
}
|
||||
dir.close();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace BookManager {
|
||||
|
||||
std::string getBookCachePath(const std::string& bookPath) { return computeCachePath(bookPath); }
|
||||
|
||||
bool isArchived(const std::string& bookPath) {
|
||||
return bookPath.rfind(std::string(ARCHIVE_ROOT) + "/", 0) == 0;
|
||||
}
|
||||
|
||||
bool archiveBook(const std::string& bookPath) {
|
||||
if (isArchived(bookPath)) {
|
||||
LOG_ERR("BKMGR", "Book is already archived: %s", bookPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string destPath = std::string(ARCHIVE_ROOT) + bookPath;
|
||||
ensureParentDirs(destPath);
|
||||
|
||||
if (!Storage.rename(bookPath.c_str(), destPath.c_str())) {
|
||||
LOG_ERR("BKMGR", "Failed to move book to archive: %s -> %s", bookPath.c_str(), destPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rename cache directory to match the new book path hash
|
||||
const std::string oldCache = computeCachePath(bookPath);
|
||||
const std::string newCache = computeCachePath(destPath);
|
||||
if (oldCache != newCache && Storage.exists(oldCache.c_str())) {
|
||||
if (!Storage.rename(oldCache.c_str(), newCache.c_str())) {
|
||||
LOG_ERR("BKMGR", "Failed to rename cache dir: %s -> %s", oldCache.c_str(), newCache.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
RECENT_BOOKS.removeBook(bookPath);
|
||||
LOG_DBG("BKMGR", "Archived: %s -> %s", bookPath.c_str(), destPath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Strip "/.archive" prefix to get original path
|
||||
std::string destPath = archivePath.substr(strlen(ARCHIVE_ROOT));
|
||||
if (destPath.empty() || destPath[0] != '/') {
|
||||
destPath = "/" + destPath;
|
||||
}
|
||||
|
||||
// Check if original parent directory exists, fall back to root
|
||||
const auto lastSlash = destPath.find_last_of('/');
|
||||
std::string parentDir = (lastSlash != std::string::npos && lastSlash > 0) ? destPath.substr(0, lastSlash) : "/";
|
||||
if (!Storage.exists(parentDir.c_str())) {
|
||||
const auto filename = destPath.substr(lastSlash + 1);
|
||||
destPath = "/" + filename;
|
||||
LOG_DBG("BKMGR", "Original dir gone, unarchiving to root: %s", destPath.c_str());
|
||||
}
|
||||
|
||||
ensureParentDirs(destPath);
|
||||
|
||||
if (!Storage.rename(archivePath.c_str(), destPath.c_str())) {
|
||||
LOG_ERR("BKMGR", "Failed to move book from archive: %s -> %s", archivePath.c_str(), destPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rename cache directory
|
||||
const std::string oldCache = computeCachePath(archivePath);
|
||||
const std::string newCache = computeCachePath(destPath);
|
||||
if (oldCache != newCache && Storage.exists(oldCache.c_str())) {
|
||||
if (!Storage.rename(oldCache.c_str(), newCache.c_str())) {
|
||||
LOG_ERR("BKMGR", "Failed to rename cache dir: %s -> %s", oldCache.c_str(), newCache.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
RECENT_BOOKS.removeBook(archivePath);
|
||||
LOG_DBG("BKMGR", "Unarchived: %s -> %s", archivePath.c_str(), destPath.c_str());
|
||||
if (unarchivedPath) *unarchivedPath = destPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool deleteBook(const std::string& bookPath) {
|
||||
// Delete the book file
|
||||
if (Storage.exists(bookPath.c_str())) {
|
||||
if (!Storage.remove(bookPath.c_str())) {
|
||||
LOG_ERR("BKMGR", "Failed to delete book file: %s", bookPath.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete cache directory
|
||||
const std::string cachePath = computeCachePath(bookPath);
|
||||
if (Storage.exists(cachePath.c_str())) {
|
||||
Storage.removeDir(cachePath.c_str());
|
||||
}
|
||||
|
||||
RECENT_BOOKS.removeBook(bookPath);
|
||||
LOG_DBG("BKMGR", "Deleted book: %s", bookPath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool deleteBookCache(const std::string& bookPath) {
|
||||
const std::string cachePath = computeCachePath(bookPath);
|
||||
if (Storage.exists(cachePath.c_str())) {
|
||||
if (!Storage.removeDir(cachePath.c_str())) {
|
||||
LOG_ERR("BKMGR", "Failed to delete cache: %s", cachePath.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
RECENT_BOOKS.removeBook(bookPath);
|
||||
LOG_DBG("BKMGR", "Deleted cache for: %s", bookPath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool reindexBook(const std::string& bookPath, bool alsoRegenerateCovers) {
|
||||
const std::string cachePath = computeCachePath(bookPath);
|
||||
if (!Storage.exists(cachePath.c_str())) {
|
||||
LOG_DBG("BKMGR", "No cache to reindex for: %s", bookPath.c_str());
|
||||
RECENT_BOOKS.removeBook(bookPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto prefix = getCachePrefix(bookPath);
|
||||
|
||||
if (prefix == "epub_") {
|
||||
// Delete sections directory
|
||||
const std::string sectionsPath = cachePath + "/sections";
|
||||
if (Storage.exists(sectionsPath.c_str())) {
|
||||
Storage.removeDir(sectionsPath.c_str());
|
||||
}
|
||||
// Delete book.bin (spine/TOC metadata)
|
||||
const std::string bookBin = cachePath + "/book.bin";
|
||||
if (Storage.exists(bookBin.c_str())) {
|
||||
Storage.remove(bookBin.c_str());
|
||||
}
|
||||
// Delete CSS cache
|
||||
const std::string cssCache = cachePath + "/css_rules.cache";
|
||||
if (Storage.exists(cssCache.c_str())) {
|
||||
Storage.remove(cssCache.c_str());
|
||||
}
|
||||
} else if (prefix == "txt_") {
|
||||
// Delete page index
|
||||
const std::string indexBin = cachePath + "/index.bin";
|
||||
if (Storage.exists(indexBin.c_str())) {
|
||||
Storage.remove(indexBin.c_str());
|
||||
}
|
||||
} else if (prefix == "xtc_") {
|
||||
// XTC is pre-indexed; only covers/thumbs are cached
|
||||
// Nothing to delete for sections
|
||||
}
|
||||
|
||||
if (alsoRegenerateCovers) {
|
||||
deleteCoverFiles(cachePath);
|
||||
}
|
||||
|
||||
RECENT_BOOKS.removeBook(bookPath);
|
||||
LOG_DBG("BKMGR", "Reindexed (covers=%d): %s", alsoRegenerateCovers, bookPath.c_str());
|
||||
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
|
||||
39
src/util/BookManager.h
Normal file
39
src/util/BookManager.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace BookManager {
|
||||
|
||||
// Compute the cache directory path for a book (e.g. "/.crosspoint/epub_12345")
|
||||
std::string getBookCachePath(const std::string& bookPath);
|
||||
|
||||
// Move a book to /.archive/ preserving directory structure.
|
||||
// Renames the cache dir to match the new path hash. Removes from recents.
|
||||
// Returns true on success.
|
||||
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.
|
||||
// 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);
|
||||
|
||||
// Delete only the cache directory for a book and remove from recents.
|
||||
bool deleteBookCache(const std::string& bookPath);
|
||||
|
||||
// Clear indexed data from cache, preserving progress.
|
||||
// If alsoRegenerateCovers is true, also deletes cover/thumbnail BMPs.
|
||||
// Removes from recents.
|
||||
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