Add individual book cache clearing with preserve progress option
- Add "Clear Cache" option to book action menu in MyLibrary (both Recent and Files tabs) - Prompt user to preserve reading progress when clearing cache - Always preserve bookmarks when clearing individual book cache - Add preserve progress option to system-level Clear Cache in Settings - Implement BookManager::clearBookCache() for selective cache clearing
This commit is contained in:
parent
5464d9de3a
commit
448ce55bb4
@ -303,6 +303,85 @@ bool BookManager::deleteBook(const std::string& bookPath, bool isArchived) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool BookManager::clearBookCache(const std::string& bookPath, bool preserveProgress) {
|
||||||
|
Serial.printf("[%lu] [%s] Clearing cache for: %s (preserveProgress=%d)\n", millis(), LOG_TAG, bookPath.c_str(),
|
||||||
|
preserveProgress);
|
||||||
|
|
||||||
|
const std::string cacheDir = getCacheDir(bookPath);
|
||||||
|
if (cacheDir.empty()) {
|
||||||
|
Serial.printf("[%lu] [%s] No cache directory for unsupported format\n", millis(), LOG_TAG);
|
||||||
|
return true; // Nothing to clear, not an error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SdMan.exists(cacheDir.c_str())) {
|
||||||
|
Serial.printf("[%lu] [%s] Cache directory doesn't exist: %s\n", millis(), LOG_TAG, cacheDir.c_str());
|
||||||
|
return true; // Nothing to clear, not an error
|
||||||
|
}
|
||||||
|
|
||||||
|
FsFile dir = SdMan.open(cacheDir.c_str());
|
||||||
|
if (!dir || !dir.isDirectory()) {
|
||||||
|
Serial.printf("[%lu] [%s] Failed to open cache directory\n", millis(), LOG_TAG);
|
||||||
|
if (dir) dir.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Files to preserve (always keep bookmarks, optionally keep progress)
|
||||||
|
const auto shouldPreserve = [preserveProgress](const char* name) {
|
||||||
|
// Always preserve bookmarks
|
||||||
|
if (strcmp(name, "bookmarks.bin") == 0) return true;
|
||||||
|
// Optionally preserve progress
|
||||||
|
if (preserveProgress && strcmp(name, "progress.bin") == 0) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
int deletedCount = 0;
|
||||||
|
int failedCount = 0;
|
||||||
|
char name[128];
|
||||||
|
|
||||||
|
// First pass: delete files (not directories)
|
||||||
|
for (FsFile entry = dir.openNextFile(); entry; entry = dir.openNextFile()) {
|
||||||
|
entry.getName(name, sizeof(name));
|
||||||
|
const bool isDir = entry.isDirectory();
|
||||||
|
entry.close();
|
||||||
|
|
||||||
|
if (!isDir && !shouldPreserve(name)) {
|
||||||
|
std::string fullPath = cacheDir + "/" + name;
|
||||||
|
if (SdMan.remove(fullPath.c_str())) {
|
||||||
|
deletedCount++;
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [%s] Failed to delete: %s\n", millis(), LOG_TAG, fullPath.c_str());
|
||||||
|
failedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dir.close();
|
||||||
|
|
||||||
|
// Second pass: delete subdirectories (like "sections/")
|
||||||
|
dir = SdMan.open(cacheDir.c_str());
|
||||||
|
if (dir && dir.isDirectory()) {
|
||||||
|
for (FsFile entry = dir.openNextFile(); entry; entry = dir.openNextFile()) {
|
||||||
|
entry.getName(name, sizeof(name));
|
||||||
|
const bool isDir = entry.isDirectory();
|
||||||
|
entry.close();
|
||||||
|
|
||||||
|
if (isDir) {
|
||||||
|
std::string fullPath = cacheDir + "/" + name;
|
||||||
|
if (SdMan.removeDir(fullPath.c_str())) {
|
||||||
|
deletedCount++;
|
||||||
|
Serial.printf("[%lu] [%s] Deleted subdirectory: %s\n", millis(), LOG_TAG, fullPath.c_str());
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [%s] Failed to delete subdirectory: %s\n", millis(), LOG_TAG, fullPath.c_str());
|
||||||
|
failedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dir.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [%s] Cache cleared: %d items deleted, %d failed\n", millis(), LOG_TAG, deletedCount, failedCount);
|
||||||
|
return failedCount == 0;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<std::string> BookManager::listArchivedBooks() {
|
std::vector<std::string> BookManager::listArchivedBooks() {
|
||||||
std::vector<std::string> archivedBooks;
|
std::vector<std::string> archivedBooks;
|
||||||
|
|
||||||
|
|||||||
@ -57,6 +57,14 @@ class BookManager {
|
|||||||
*/
|
*/
|
||||||
static std::string getCacheDir(const std::string& bookPath);
|
static std::string getCacheDir(const std::string& bookPath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cache for a single book, optionally preserving reading progress
|
||||||
|
* @param bookPath Full path to the book file
|
||||||
|
* @param preserveProgress If true, keeps progress.bin and bookmarks.bin
|
||||||
|
* @return true if successful (or if no cache exists)
|
||||||
|
*/
|
||||||
|
static bool clearBookCache(const std::string& bookPath, bool preserveProgress);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Extract filename from a full path
|
// Extract filename from a full path
|
||||||
static std::string getFilename(const std::string& path);
|
static std::string getFilename(const std::string& path);
|
||||||
|
|||||||
@ -503,22 +503,29 @@ void MyLibraryActivity::executeAction() {
|
|||||||
} else if (selectedAction == ActionType::RemoveFromRecents) {
|
} else if (selectedAction == ActionType::RemoveFromRecents) {
|
||||||
// Just remove from recents list, don't touch the file
|
// Just remove from recents list, don't touch the file
|
||||||
success = RECENT_BOOKS.removeBook(actionTargetPath);
|
success = RECENT_BOOKS.removeBook(actionTargetPath);
|
||||||
|
} else if (selectedAction == ActionType::ClearCache) {
|
||||||
|
// Clear cache for this book, optionally preserving progress
|
||||||
|
success = BookManager::clearBookCache(actionTargetPath, clearCachePreserveProgress);
|
||||||
|
// Also clear thumbnail existence cache since thumbnails may have been deleted
|
||||||
|
clearThumbExistsCache();
|
||||||
}
|
}
|
||||||
// Note: ClearAllRecents is handled directly in loop() via ClearAllRecentsConfirming state
|
// Note: ClearAllRecents is handled directly in loop() via ClearAllRecentsConfirming state
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
// Reload data
|
// Reload data
|
||||||
loadRecentBooks();
|
loadRecentBooks();
|
||||||
if (selectedAction != ActionType::RemoveFromRecents) {
|
if (selectedAction != ActionType::RemoveFromRecents && selectedAction != ActionType::ClearCache) {
|
||||||
loadFiles(); // Only reload files for Archive/Delete
|
loadFiles(); // Only reload files for Archive/Delete (not needed for cache clear)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust selector if needed
|
// Adjust selector if needed (not needed for ClearCache since item count doesn't change)
|
||||||
const int itemCount = getCurrentItemCount();
|
if (selectedAction != ActionType::ClearCache) {
|
||||||
if (selectorIndex >= itemCount && itemCount > 0) {
|
const int itemCount = getCurrentItemCount();
|
||||||
selectorIndex = itemCount - 1;
|
if (selectorIndex >= itemCount && itemCount > 0) {
|
||||||
} else if (itemCount == 0) {
|
selectorIndex = itemCount - 1;
|
||||||
selectorIndex = 0;
|
} else if (itemCount == 0) {
|
||||||
|
selectorIndex = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -577,8 +584,8 @@ void MyLibraryActivity::executeListAction() {
|
|||||||
void MyLibraryActivity::loop() {
|
void MyLibraryActivity::loop() {
|
||||||
// Handle action menu state
|
// Handle action menu state
|
||||||
if (uiState == UIState::ActionMenu) {
|
if (uiState == UIState::ActionMenu) {
|
||||||
// Menu has 4 options in Recent tab, 2 options in Files tab
|
// Menu has 5 options in Recent tab, 3 options in Files tab
|
||||||
const int maxMenuSelection = (currentTab == Tab::Recent) ? 3 : 1;
|
const int maxMenuSelection = (currentTab == Tab::Recent) ? 4 : 2;
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
uiState = UIState::Normal;
|
uiState = UIState::Normal;
|
||||||
@ -608,7 +615,7 @@ void MyLibraryActivity::loop() {
|
|||||||
|
|
||||||
// Map menu selection to action type
|
// Map menu selection to action type
|
||||||
if (currentTab == Tab::Recent) {
|
if (currentTab == Tab::Recent) {
|
||||||
// Recent tab: Archive(0), Delete(1), Remove from Recents(2), Clear All Recents(3)
|
// Recent tab: Archive(0), Delete(1), Clear Cache(2), Remove from Recents(3), Clear All Recents(4)
|
||||||
switch (menuSelection) {
|
switch (menuSelection) {
|
||||||
case 0:
|
case 0:
|
||||||
selectedAction = ActionType::Archive;
|
selectedAction = ActionType::Archive;
|
||||||
@ -617,20 +624,37 @@ void MyLibraryActivity::loop() {
|
|||||||
selectedAction = ActionType::Delete;
|
selectedAction = ActionType::Delete;
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
selectedAction = ActionType::RemoveFromRecents;
|
selectedAction = ActionType::ClearCache;
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
|
selectedAction = ActionType::RemoveFromRecents;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
selectedAction = ActionType::ClearAllRecents;
|
selectedAction = ActionType::ClearAllRecents;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Files tab: Archive(0), Delete(1)
|
// Files tab: Archive(0), Delete(1), Clear Cache(2)
|
||||||
selectedAction = (menuSelection == 0) ? ActionType::Archive : ActionType::Delete;
|
switch (menuSelection) {
|
||||||
|
case 0:
|
||||||
|
selectedAction = ActionType::Archive;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
selectedAction = ActionType::Delete;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
selectedAction = ActionType::ClearCache;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear All Recents needs its own confirmation dialog
|
// Clear All Recents needs its own confirmation dialog
|
||||||
if (selectedAction == ActionType::ClearAllRecents) {
|
if (selectedAction == ActionType::ClearAllRecents) {
|
||||||
uiState = UIState::ClearAllRecentsConfirming;
|
uiState = UIState::ClearAllRecentsConfirming;
|
||||||
|
} else if (selectedAction == ActionType::ClearCache) {
|
||||||
|
// Clear Cache shows options dialog first
|
||||||
|
clearCachePreserveProgress = true; // Default to preserving progress
|
||||||
|
uiState = UIState::ClearCacheOptionsConfirming;
|
||||||
} else {
|
} else {
|
||||||
uiState = UIState::Confirming;
|
uiState = UIState::Confirming;
|
||||||
}
|
}
|
||||||
@ -735,6 +759,30 @@ void MyLibraryActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle clear cache options confirmation state
|
||||||
|
if (uiState == UIState::ClearCacheOptionsConfirming) {
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
|
uiState = UIState::ActionMenu;
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Up/Down toggle between Yes/No for preserve progress
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Up) ||
|
||||||
|
mappedInput.wasReleased(MappedInputManager::Button::Down)) {
|
||||||
|
clearCachePreserveProgress = !clearCachePreserveProgress;
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
|
executeAction();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Normal state handling
|
// Normal state handling
|
||||||
const int itemCount = getCurrentItemCount();
|
const int itemCount = getCurrentItemCount();
|
||||||
const int pageItems = getPageItems();
|
const int pageItems = getPageItems();
|
||||||
@ -1303,6 +1351,12 @@ void MyLibraryActivity::render() const {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (uiState == UIState::ClearCacheOptionsConfirming) {
|
||||||
|
renderClearCacheOptionsConfirmation();
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate bezel-adjusted margins
|
// Calculate bezel-adjusted margins
|
||||||
const int bezelTop = renderer.getBezelOffsetTop();
|
const int bezelTop = renderer.getBezelOffsetTop();
|
||||||
const int bezelBottom = renderer.getBezelOffsetBottom();
|
const int bezelBottom = renderer.getBezelOffsetBottom();
|
||||||
@ -1661,40 +1715,46 @@ void MyLibraryActivity::renderActionMenu() const {
|
|||||||
renderer.truncatedText(UI_10_FONT_ID, actionTargetName.c_str(), pageWidth - 40 - bezelLeft - bezelRight);
|
renderer.truncatedText(UI_10_FONT_ID, actionTargetName.c_str(), pageWidth - 40 - bezelLeft - bezelRight);
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, filenameY, truncatedName.c_str());
|
renderer.drawCenteredText(UI_10_FONT_ID, filenameY, truncatedName.c_str());
|
||||||
|
|
||||||
// Menu options - 4 for Recent tab, 2 for Files tab
|
// Menu options - 5 for Recent tab, 3 for Files tab
|
||||||
const bool isRecentTab = (currentTab == Tab::Recent);
|
const bool isRecentTab = (currentTab == Tab::Recent);
|
||||||
const int menuItemCount = isRecentTab ? 4 : 2;
|
const int menuItemCount = isRecentTab ? 5 : 3;
|
||||||
constexpr int menuLineHeight = 35;
|
constexpr int menuLineHeight = 35;
|
||||||
constexpr int menuItemWidth = 160;
|
constexpr int menuItemWidth = 160;
|
||||||
const int menuX = (pageWidth - menuItemWidth) / 2;
|
const int menuX = (pageWidth - menuItemWidth) / 2;
|
||||||
const int menuStartY = pageHeight / 2 - (menuItemCount * menuLineHeight) / 2;
|
const int menuStartY = pageHeight / 2 - (menuItemCount * menuLineHeight) / 2;
|
||||||
|
|
||||||
// Archive option
|
// Archive option (index 0)
|
||||||
if (menuSelection == 0) {
|
if (menuSelection == 0) {
|
||||||
renderer.fillRect(menuX - 10, menuStartY - 5, menuItemWidth + 20, menuLineHeight);
|
renderer.fillRect(menuX - 10, menuStartY - 5, menuItemWidth + 20, menuLineHeight);
|
||||||
}
|
}
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, menuStartY, "Archive", menuSelection != 0);
|
renderer.drawCenteredText(UI_10_FONT_ID, menuStartY, "Archive", menuSelection != 0);
|
||||||
|
|
||||||
// Delete option
|
// Delete option (index 1)
|
||||||
if (menuSelection == 1) {
|
if (menuSelection == 1) {
|
||||||
renderer.fillRect(menuX - 10, menuStartY + menuLineHeight - 5, menuItemWidth + 20, menuLineHeight);
|
renderer.fillRect(menuX - 10, menuStartY + menuLineHeight - 5, menuItemWidth + 20, menuLineHeight);
|
||||||
}
|
}
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, menuStartY + menuLineHeight, "Delete", menuSelection != 1);
|
renderer.drawCenteredText(UI_10_FONT_ID, menuStartY + menuLineHeight, "Delete", menuSelection != 1);
|
||||||
|
|
||||||
|
// Clear Cache option (index 2) - available in both tabs
|
||||||
|
if (menuSelection == 2) {
|
||||||
|
renderer.fillRect(menuX - 10, menuStartY + menuLineHeight * 2 - 5, menuItemWidth + 20, menuLineHeight);
|
||||||
|
}
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, menuStartY + menuLineHeight * 2, "Clear Cache", menuSelection != 2);
|
||||||
|
|
||||||
// Recent tab only: Remove from Recents and Clear All Recents
|
// Recent tab only: Remove from Recents and Clear All Recents
|
||||||
if (isRecentTab) {
|
if (isRecentTab) {
|
||||||
// Remove from Recents option
|
// Remove from Recents option (index 3)
|
||||||
if (menuSelection == 2) {
|
|
||||||
renderer.fillRect(menuX - 10, menuStartY + menuLineHeight * 2 - 5, menuItemWidth + 20, menuLineHeight);
|
|
||||||
}
|
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, menuStartY + menuLineHeight * 2, "Remove from Recents",
|
|
||||||
menuSelection != 2);
|
|
||||||
|
|
||||||
// Clear All Recents option
|
|
||||||
if (menuSelection == 3) {
|
if (menuSelection == 3) {
|
||||||
renderer.fillRect(menuX - 10, menuStartY + menuLineHeight * 3 - 5, menuItemWidth + 20, menuLineHeight);
|
renderer.fillRect(menuX - 10, menuStartY + menuLineHeight * 3 - 5, menuItemWidth + 20, menuLineHeight);
|
||||||
}
|
}
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, menuStartY + menuLineHeight * 3, "Clear All Recents", menuSelection != 3);
|
renderer.drawCenteredText(UI_10_FONT_ID, menuStartY + menuLineHeight * 3, "Remove from Recents",
|
||||||
|
menuSelection != 3);
|
||||||
|
|
||||||
|
// Clear All Recents option (index 4)
|
||||||
|
if (menuSelection == 4) {
|
||||||
|
renderer.fillRect(menuX - 10, menuStartY + menuLineHeight * 4 - 5, menuItemWidth + 20, menuLineHeight);
|
||||||
|
}
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, menuStartY + menuLineHeight * 4, "Clear All Recents", menuSelection != 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw side button hints (up/down navigation)
|
// Draw side button hints (up/down navigation)
|
||||||
@ -1828,6 +1888,54 @@ void MyLibraryActivity::renderClearAllRecentsConfirmation() const {
|
|||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MyLibraryActivity::renderClearCacheOptionsConfirmation() const {
|
||||||
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
// Bezel compensation
|
||||||
|
const int bezelTop = renderer.getBezelOffsetTop();
|
||||||
|
|
||||||
|
// Title
|
||||||
|
renderer.drawCenteredText(UI_12_FONT_ID, 20 + bezelTop, "Clear Book Cache", true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
|
// Show filename
|
||||||
|
const int filenameY = 60 + bezelTop;
|
||||||
|
const int bezelLeft = renderer.getBezelOffsetLeft();
|
||||||
|
const int bezelRight = renderer.getBezelOffsetRight();
|
||||||
|
auto truncatedName =
|
||||||
|
renderer.truncatedText(UI_10_FONT_ID, actionTargetName.c_str(), pageWidth - 40 - bezelLeft - bezelRight);
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, filenameY, truncatedName.c_str());
|
||||||
|
|
||||||
|
// Question text
|
||||||
|
const int questionY = pageHeight / 2 - 50;
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, questionY, "Preserve reading progress?");
|
||||||
|
|
||||||
|
// Yes/No options
|
||||||
|
constexpr int optionLineHeight = 35;
|
||||||
|
constexpr int optionWidth = 100;
|
||||||
|
const int optionX = (pageWidth - optionWidth) / 2;
|
||||||
|
const int optionStartY = questionY + 40;
|
||||||
|
|
||||||
|
// Yes option
|
||||||
|
if (clearCachePreserveProgress) {
|
||||||
|
renderer.fillRect(optionX - 10, optionStartY - 5, optionWidth + 20, optionLineHeight);
|
||||||
|
}
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, optionStartY, "Yes", !clearCachePreserveProgress);
|
||||||
|
|
||||||
|
// No option
|
||||||
|
if (!clearCachePreserveProgress) {
|
||||||
|
renderer.fillRect(optionX - 10, optionStartY + optionLineHeight - 5, optionWidth + 20, optionLineHeight);
|
||||||
|
}
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, optionStartY + optionLineHeight, "No", clearCachePreserveProgress);
|
||||||
|
|
||||||
|
// Draw side button hints (up/down navigation)
|
||||||
|
renderer.drawSideButtonHints(UI_10_FONT_ID, ">", "<");
|
||||||
|
|
||||||
|
// Draw bottom button hints
|
||||||
|
const auto labels = mappedInput.mapLabels("« Cancel", "Clear", "", "");
|
||||||
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
}
|
||||||
|
|
||||||
void MyLibraryActivity::renderBookmarksTab() const {
|
void MyLibraryActivity::renderBookmarksTab() const {
|
||||||
const auto pageWidth = renderer.getScreenWidth();
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
const int pageItems = getPageItems();
|
const int pageItems = getPageItems();
|
||||||
|
|||||||
@ -44,9 +44,10 @@ class MyLibraryActivity final : public Activity {
|
|||||||
Confirming,
|
Confirming,
|
||||||
ListActionMenu,
|
ListActionMenu,
|
||||||
ListConfirmingDelete,
|
ListConfirmingDelete,
|
||||||
ClearAllRecentsConfirming
|
ClearAllRecentsConfirming,
|
||||||
|
ClearCacheOptionsConfirming
|
||||||
};
|
};
|
||||||
enum class ActionType { Archive, Delete, RemoveFromRecents, ClearAllRecents };
|
enum class ActionType { Archive, Delete, RemoveFromRecents, ClearCache, ClearAllRecents };
|
||||||
|
|
||||||
private:
|
private:
|
||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
@ -64,6 +65,7 @@ class MyLibraryActivity final : public Activity {
|
|||||||
std::string actionTargetName;
|
std::string actionTargetName;
|
||||||
int menuSelection = 0; // 0 = Archive, 1 = Delete
|
int menuSelection = 0; // 0 = Archive, 1 = Delete
|
||||||
bool ignoreNextConfirmRelease = false; // Prevents immediate selection after long-press opens menu
|
bool ignoreNextConfirmRelease = false; // Prevents immediate selection after long-press opens menu
|
||||||
|
bool clearCachePreserveProgress = true; // For Clear Cache: whether to preserve reading progress
|
||||||
|
|
||||||
// Recent tab state
|
// Recent tab state
|
||||||
std::vector<RecentBook> recentBooks;
|
std::vector<RecentBook> recentBooks;
|
||||||
@ -153,6 +155,9 @@ class MyLibraryActivity final : public Activity {
|
|||||||
// Clear all recents confirmation
|
// Clear all recents confirmation
|
||||||
void renderClearAllRecentsConfirmation() const;
|
void renderClearAllRecentsConfirmation() const;
|
||||||
|
|
||||||
|
// Clear cache options confirmation
|
||||||
|
void renderClearCacheOptionsConfirmation() const;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit MyLibraryActivity(
|
explicit MyLibraryActivity(
|
||||||
GfxRenderer& renderer, MappedInputManager& mappedInput, const std::function<void()>& onGoHome,
|
GfxRenderer& renderer, MappedInputManager& mappedInput, const std::function<void()>& onGoHome,
|
||||||
|
|||||||
@ -4,6 +4,9 @@
|
|||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <SDCardManager.h>
|
#include <SDCardManager.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "activities/home/HomeActivity.h"
|
#include "activities/home/HomeActivity.h"
|
||||||
#include "activities/home/MyLibraryActivity.h"
|
#include "activities/home/MyLibraryActivity.h"
|
||||||
@ -19,6 +22,7 @@ void ClearCacheActivity::onEnter() {
|
|||||||
|
|
||||||
renderingMutex = xSemaphoreCreateMutex();
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
state = WARNING;
|
state = WARNING;
|
||||||
|
preserveProgress = true; // Default to preserving progress
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
|
||||||
xTaskCreate(&ClearCacheActivity::taskTrampoline, "ClearCacheActivityTask",
|
xTaskCreate(&ClearCacheActivity::taskTrampoline, "ClearCacheActivityTask",
|
||||||
@ -56,6 +60,7 @@ void ClearCacheActivity::displayTaskLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ClearCacheActivity::render() {
|
void ClearCacheActivity::render() {
|
||||||
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
const auto pageHeight = renderer.getScreenHeight();
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
// Bezel compensation
|
// Bezel compensation
|
||||||
@ -67,11 +72,32 @@ void ClearCacheActivity::render() {
|
|||||||
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, "Clear Cache", true, EpdFontFamily::BOLD);
|
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, "Clear Cache", true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
if (state == WARNING) {
|
if (state == WARNING) {
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 60, "This will clear all cached book data.", true);
|
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 70, "This will clear all cached book data.", true);
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 30, "All reading progress will be lost!", true,
|
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 45, "Books will need to be re-indexed.", true);
|
||||||
EpdFontFamily::BOLD);
|
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 10, "Books will need to be re-indexed", true);
|
// Preserve progress option
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 30, "when opened again.", true);
|
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 5, "Preserve reading progress?");
|
||||||
|
|
||||||
|
// Yes/No options
|
||||||
|
constexpr int optionLineHeight = 30;
|
||||||
|
constexpr int optionWidth = 80;
|
||||||
|
const int optionX = (pageWidth - optionWidth) / 2;
|
||||||
|
const int optionStartY = centerY + 25;
|
||||||
|
|
||||||
|
// Yes option
|
||||||
|
if (preserveProgress) {
|
||||||
|
renderer.fillRect(optionX - 10, optionStartY - 5, optionWidth + 20, optionLineHeight);
|
||||||
|
}
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, optionStartY, "Yes", !preserveProgress);
|
||||||
|
|
||||||
|
// No option
|
||||||
|
if (!preserveProgress) {
|
||||||
|
renderer.fillRect(optionX - 10, optionStartY + optionLineHeight - 5, optionWidth + 20, optionLineHeight);
|
||||||
|
}
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, optionStartY + optionLineHeight, "No", preserveProgress);
|
||||||
|
|
||||||
|
// Draw side button hints (up/down navigation)
|
||||||
|
renderer.drawSideButtonHints(UI_10_FONT_ID, ">", "<");
|
||||||
|
|
||||||
const auto labels = mappedInput.mapLabels("« Cancel", "Clear", "", "");
|
const auto labels = mappedInput.mapLabels("« Cancel", "Clear", "", "");
|
||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
@ -110,8 +136,68 @@ void ClearCacheActivity::render() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ClearCacheActivity::clearCacheDirectory(const char* dirPath) {
|
||||||
|
// Helper to check if a file should be preserved
|
||||||
|
const auto shouldPreserve = [this](const char* name) {
|
||||||
|
if (!preserveProgress) return false;
|
||||||
|
// Preserve progress and bookmarks when preserveProgress is enabled
|
||||||
|
return (strcmp(name, "progress.bin") == 0 || strcmp(name, "bookmarks.bin") == 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
FsFile dir = SdMan.open(dirPath);
|
||||||
|
if (!dir || !dir.isDirectory()) {
|
||||||
|
if (dir) dir.close();
|
||||||
|
failedCount++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char name[128];
|
||||||
|
std::vector<std::string> filesToDelete;
|
||||||
|
std::vector<std::string> dirsToDelete;
|
||||||
|
|
||||||
|
// First pass: collect files and directories to delete
|
||||||
|
for (FsFile entry = dir.openNextFile(); entry; entry = dir.openNextFile()) {
|
||||||
|
entry.getName(name, sizeof(name));
|
||||||
|
const bool isDir = entry.isDirectory();
|
||||||
|
entry.close();
|
||||||
|
|
||||||
|
std::string fullPath = std::string(dirPath) + "/" + name;
|
||||||
|
if (isDir) {
|
||||||
|
dirsToDelete.push_back(fullPath);
|
||||||
|
} else if (!shouldPreserve(name)) {
|
||||||
|
filesToDelete.push_back(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dir.close();
|
||||||
|
|
||||||
|
// Delete files
|
||||||
|
for (const auto& path : filesToDelete) {
|
||||||
|
if (SdMan.remove(path.c_str())) {
|
||||||
|
clearedCount++;
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [CLEAR_CACHE] Failed to delete file: %s\n", millis(), path.c_str());
|
||||||
|
failedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete subdirectories (like "sections/")
|
||||||
|
for (const auto& path : dirsToDelete) {
|
||||||
|
if (SdMan.removeDir(path.c_str())) {
|
||||||
|
clearedCount++;
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [CLEAR_CACHE] Failed to delete dir: %s\n", millis(), path.c_str());
|
||||||
|
failedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not preserving progress, try to remove the now-empty directory
|
||||||
|
if (!preserveProgress) {
|
||||||
|
SdMan.rmdir(dirPath); // This will fail if directory is not empty, which is fine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ClearCacheActivity::clearCache() {
|
void ClearCacheActivity::clearCache() {
|
||||||
Serial.printf("[%lu] [CLEAR_CACHE] Clearing cache...\n", millis());
|
Serial.printf("[%lu] [CLEAR_CACHE] Clearing cache (preserveProgress=%d)...\n", millis(), preserveProgress);
|
||||||
|
|
||||||
// Open .crosspoint directory
|
// Open .crosspoint directory
|
||||||
auto root = SdMan.open("/.crosspoint");
|
auto root = SdMan.open("/.crosspoint");
|
||||||
@ -127,35 +213,31 @@ void ClearCacheActivity::clearCache() {
|
|||||||
failedCount = 0;
|
failedCount = 0;
|
||||||
char name[128];
|
char name[128];
|
||||||
|
|
||||||
// Iterate through all entries in the directory
|
// Collect all book cache directories first
|
||||||
|
std::vector<std::string> cacheDirs;
|
||||||
for (auto file = root.openNextFile(); file; file = root.openNextFile()) {
|
for (auto file = root.openNextFile(); file; file = root.openNextFile()) {
|
||||||
file.getName(name, sizeof(name));
|
file.getName(name, sizeof(name));
|
||||||
String itemName(name);
|
String itemName(name);
|
||||||
|
|
||||||
// Only delete directories starting with epub_ or txt_
|
// Only process directories starting with epub_ or txt_
|
||||||
if (file.isDirectory() && (itemName.startsWith("epub_") || itemName.startsWith("txt_"))) {
|
if (file.isDirectory() && (itemName.startsWith("epub_") || itemName.startsWith("txt_"))) {
|
||||||
String fullPath = "/.crosspoint/" + itemName;
|
cacheDirs.push_back("/.crosspoint/" + std::string(name));
|
||||||
Serial.printf("[%lu] [CLEAR_CACHE] Removing cache: %s\n", millis(), fullPath.c_str());
|
|
||||||
|
|
||||||
file.close(); // Close before attempting to delete
|
|
||||||
|
|
||||||
if (SdMan.removeDir(fullPath.c_str())) {
|
|
||||||
clearedCount++;
|
|
||||||
} else {
|
|
||||||
Serial.printf("[%lu] [CLEAR_CACHE] Failed to remove: %s\n", millis(), fullPath.c_str());
|
|
||||||
failedCount++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
file.close();
|
|
||||||
}
|
}
|
||||||
|
file.close();
|
||||||
}
|
}
|
||||||
root.close();
|
root.close();
|
||||||
|
|
||||||
|
// Now clear each cache directory
|
||||||
|
for (const auto& cacheDir : cacheDirs) {
|
||||||
|
Serial.printf("[%lu] [CLEAR_CACHE] Clearing: %s\n", millis(), cacheDir.c_str());
|
||||||
|
clearCacheDirectory(cacheDir.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
// Also clear in-memory caches since disk cache is gone
|
// Also clear in-memory caches since disk cache is gone
|
||||||
HomeActivity::freeCoverBufferIfAllocated();
|
HomeActivity::freeCoverBufferIfAllocated();
|
||||||
MyLibraryActivity::clearThumbExistsCache();
|
MyLibraryActivity::clearThumbExistsCache();
|
||||||
|
|
||||||
Serial.printf("[%lu] [CLEAR_CACHE] Cache cleared: %d removed, %d failed\n", millis(), clearedCount, failedCount);
|
Serial.printf("[%lu] [CLEAR_CACHE] Cache cleared: %d items removed, %d failed\n", millis(), clearedCount, failedCount);
|
||||||
|
|
||||||
state = SUCCESS;
|
state = SUCCESS;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
@ -163,8 +245,17 @@ void ClearCacheActivity::clearCache() {
|
|||||||
|
|
||||||
void ClearCacheActivity::loop() {
|
void ClearCacheActivity::loop() {
|
||||||
if (state == WARNING) {
|
if (state == WARNING) {
|
||||||
|
// Up/Down toggle preserve progress option
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
||||||
|
mappedInput.wasPressed(MappedInputManager::Button::Down)) {
|
||||||
|
preserveProgress = !preserveProgress;
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
Serial.printf("[%lu] [CLEAR_CACHE] User confirmed, starting cache clear\n", millis());
|
Serial.printf("[%lu] [CLEAR_CACHE] User confirmed (preserveProgress=%d), starting cache clear\n", millis(),
|
||||||
|
preserveProgress);
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
state = CLEARING;
|
state = CLEARING;
|
||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
|
|||||||
@ -29,9 +29,11 @@ class ClearCacheActivity final : public ActivityWithSubactivity {
|
|||||||
|
|
||||||
int clearedCount = 0;
|
int clearedCount = 0;
|
||||||
int failedCount = 0;
|
int failedCount = 0;
|
||||||
|
bool preserveProgress = true; // Whether to keep progress.bin and bookmarks.bin
|
||||||
|
|
||||||
static void taskTrampoline(void* param);
|
static void taskTrampoline(void* param);
|
||||||
[[noreturn]] void displayTaskLoop();
|
[[noreturn]] void displayTaskLoop();
|
||||||
void render();
|
void render();
|
||||||
void clearCache();
|
void clearCache();
|
||||||
|
void clearCacheDirectory(const char* dirPath); // Helper to clear a single book's cache
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user