feat: Recents view improvements with badges, removal, and clearing
- Add pill badge system for displaying file type and suffix tags - Add "Remove from Recents" option to remove individual books - Add "Clear All Recents" option to clear entire recents list - Add clearThumbExistsCache() for cache invalidation - Create BadgeConfig.h for customizable badge mappings - Add extractBookTags() utility for parsing filename badges - Add drawPillBadge() component for rendering badges
This commit is contained in:
@@ -31,8 +31,6 @@ std::string getMicroThumbPathForBook(const std::string& bookPath) {
|
||||
|
||||
if (StringUtils::checkFileExtension(bookPath, ".epub")) {
|
||||
return "/.crosspoint/epub_" + std::to_string(hash) + "/micro_thumb.bmp";
|
||||
} else if (StringUtils::checkFileExtension(bookPath, ".xtc") || StringUtils::checkFileExtension(bookPath, ".xtch")) {
|
||||
return "/.crosspoint/xtc_" + std::to_string(hash) + "/micro_thumb.bmp";
|
||||
} else if (StringUtils::checkFileExtension(bookPath, ".txt") || StringUtils::checkFileExtension(bookPath, ".TXT")) {
|
||||
return "/.crosspoint/txt_" + std::to_string(hash) + "/micro_thumb.bmp";
|
||||
}
|
||||
@@ -234,7 +232,7 @@ void ListViewActivity::render() const {
|
||||
}
|
||||
|
||||
// Use full width if no thumbnail
|
||||
const int availableWidth = hasThumb ? textMaxWidth : (pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
|
||||
const int baseAvailableWidth = hasThumb ? textMaxWidth : (pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
|
||||
|
||||
// Line 1: Title
|
||||
std::string title = book.title;
|
||||
@@ -250,12 +248,60 @@ void ListViewActivity::render() const {
|
||||
title.resize(dot);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract tags for badges (only if we'll show them - when NOT selected)
|
||||
constexpr int badgeSpacing = 4; // Gap between badges
|
||||
constexpr int badgePadding = 10; // Horizontal padding inside badge (5 each side)
|
||||
constexpr int badgeToThumbGap = 8; // Gap between rightmost badge and cover art
|
||||
int totalBadgeWidth = 0;
|
||||
BookTags tags;
|
||||
|
||||
if (!isSelected) {
|
||||
tags = StringUtils::extractBookTags(book.path);
|
||||
if (!tags.extensionTag.empty()) {
|
||||
totalBadgeWidth += renderer.getTextWidth(SMALL_FONT_ID, tags.extensionTag.c_str()) + badgePadding;
|
||||
}
|
||||
if (!tags.suffixTag.empty()) {
|
||||
if (totalBadgeWidth > 0) {
|
||||
totalBadgeWidth += badgeSpacing;
|
||||
}
|
||||
totalBadgeWidth += renderer.getTextWidth(SMALL_FONT_ID, tags.suffixTag.c_str()) + badgePadding;
|
||||
}
|
||||
}
|
||||
|
||||
// When selected, use full width (no badges shown)
|
||||
// When not selected, reserve space for badges at the right edge (plus gap to thumbnail)
|
||||
const int badgeReservedWidth = totalBadgeWidth > 0 ? (totalBadgeWidth + badgeSpacing + badgeToThumbGap) : 0;
|
||||
const int availableWidth = isSelected ? baseAvailableWidth : (baseAvailableWidth - badgeReservedWidth);
|
||||
auto truncatedBookTitle = renderer.truncatedText(UI_12_FONT_ID, title.c_str(), availableWidth);
|
||||
renderer.drawText(UI_12_FONT_ID, LEFT_MARGIN, y + 2, truncatedBookTitle.c_str(), !isSelected);
|
||||
|
||||
// Draw badges right-aligned (near thumbnail or right edge) - only when NOT selected
|
||||
if (!isSelected && totalBadgeWidth > 0) {
|
||||
// Position badges at the right edge of the available text area (with gap to thumbnail)
|
||||
const int badgeAreaRight = LEFT_MARGIN + baseAvailableWidth - badgeToThumbGap;
|
||||
int badgeX = badgeAreaRight - totalBadgeWidth;
|
||||
|
||||
// Center badge vertically within title line height
|
||||
const int titleLineHeight = renderer.getLineHeight(UI_12_FONT_ID);
|
||||
const int badgeLineHeight = renderer.getLineHeight(SMALL_FONT_ID);
|
||||
const int badgeVerticalPadding = 4; // 2px padding top + bottom in badge
|
||||
const int badgeHeight = badgeLineHeight + badgeVerticalPadding;
|
||||
const int badgeY = y + 2 + (titleLineHeight - badgeHeight) / 2;
|
||||
|
||||
if (!tags.extensionTag.empty()) {
|
||||
int badgeWidth = ScreenComponents::drawPillBadge(renderer, badgeX, badgeY, tags.extensionTag.c_str(),
|
||||
SMALL_FONT_ID, false);
|
||||
badgeX += badgeWidth + badgeSpacing;
|
||||
}
|
||||
if (!tags.suffixTag.empty()) {
|
||||
ScreenComponents::drawPillBadge(renderer, badgeX, badgeY, tags.suffixTag.c_str(), SMALL_FONT_ID, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Line 2: Author
|
||||
if (!book.author.empty()) {
|
||||
auto truncatedAuthor = renderer.truncatedText(UI_10_FONT_ID, book.author.c_str(), availableWidth);
|
||||
auto truncatedAuthor = renderer.truncatedText(UI_10_FONT_ID, book.author.c_str(), baseAvailableWidth);
|
||||
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, y + 32, truncatedAuthor.c_str(), !isSelected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "BookListStore.h"
|
||||
#include "BookManager.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "HomeActivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "ScreenComponents.h"
|
||||
@@ -19,6 +20,16 @@
|
||||
// Static thumbnail existence cache definition
|
||||
ThumbExistsCache MyLibraryActivity::thumbExistsCache[MyLibraryActivity::MAX_THUMB_CACHE];
|
||||
|
||||
void MyLibraryActivity::clearThumbExistsCache() {
|
||||
for (int i = 0; i < MAX_THUMB_CACHE; i++) {
|
||||
thumbExistsCache[i].bookPath.clear();
|
||||
thumbExistsCache[i].thumbPath.clear();
|
||||
thumbExistsCache[i].checked = false;
|
||||
thumbExistsCache[i].exists = false;
|
||||
}
|
||||
Serial.printf("[%lu] [MYLIB] Thumbnail existence cache cleared\n", millis());
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Layout constants
|
||||
constexpr int TAB_BAR_Y = 15;
|
||||
@@ -33,13 +44,11 @@ constexpr int THUMB_RIGHT_MARGIN = 50; // Space from right edge for thumbnail
|
||||
|
||||
// Helper function to get the micro-thumb path for a book based on its file path
|
||||
std::string getMicroThumbPathForBook(const std::string& bookPath) {
|
||||
// Calculate cache path using same hash method as Epub/Xtc/Txt classes
|
||||
// Calculate cache path using same hash method as Epub/Txt classes
|
||||
const size_t hash = std::hash<std::string>{}(bookPath);
|
||||
|
||||
if (StringUtils::checkFileExtension(bookPath, ".epub")) {
|
||||
return "/.crosspoint/epub_" + std::to_string(hash) + "/micro_thumb.bmp";
|
||||
} else if (StringUtils::checkFileExtension(bookPath, ".xtc") || StringUtils::checkFileExtension(bookPath, ".xtch")) {
|
||||
return "/.crosspoint/xtc_" + std::to_string(hash) + "/micro_thumb.bmp";
|
||||
} else if (StringUtils::checkFileExtension(bookPath, ".txt") || StringUtils::checkFileExtension(bookPath, ".TXT")) {
|
||||
return "/.crosspoint/txt_" + std::to_string(hash) + "/micro_thumb.bmp";
|
||||
}
|
||||
@@ -187,6 +196,8 @@ void MyLibraryActivity::onExit() {
|
||||
// EPD
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
// Log stack high-water mark before deleting task (stack size: 4096 bytes)
|
||||
LOG_STACK_WATERMARK("MyLibraryActivity", displayTaskHandle);
|
||||
vTaskDelete(displayTaskHandle);
|
||||
displayTaskHandle = nullptr;
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS); // Let idle task free stack
|
||||
@@ -249,14 +260,20 @@ void MyLibraryActivity::executeAction() {
|
||||
|
||||
if (selectedAction == ActionType::Archive) {
|
||||
success = BookManager::archiveBook(actionTargetPath);
|
||||
} else {
|
||||
} else if (selectedAction == ActionType::Delete) {
|
||||
success = BookManager::deleteBook(actionTargetPath);
|
||||
} else if (selectedAction == ActionType::RemoveFromRecents) {
|
||||
// Just remove from recents list, don't touch the file
|
||||
success = RECENT_BOOKS.removeBook(actionTargetPath);
|
||||
}
|
||||
// Note: ClearAllRecents is handled directly in loop() via ClearAllRecentsConfirming state
|
||||
|
||||
if (success) {
|
||||
// Reload data
|
||||
loadRecentBooks();
|
||||
loadFiles();
|
||||
if (selectedAction != ActionType::RemoveFromRecents) {
|
||||
loadFiles(); // Only reload files for Archive/Delete
|
||||
}
|
||||
|
||||
// Adjust selector if needed
|
||||
const int itemCount = getCurrentItemCount();
|
||||
@@ -321,6 +338,9 @@ void MyLibraryActivity::executeListAction() {
|
||||
void MyLibraryActivity::loop() {
|
||||
// Handle action menu state
|
||||
if (uiState == UIState::ActionMenu) {
|
||||
// Menu has 4 options in Recent tab, 2 options in Files tab
|
||||
const int maxMenuSelection = (currentTab == Tab::Recent) ? 3 : 1;
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
uiState = UIState::Normal;
|
||||
ignoreNextConfirmRelease = false;
|
||||
@@ -329,13 +349,13 @@ void MyLibraryActivity::loop() {
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Up)) {
|
||||
menuSelection = 0; // Archive
|
||||
menuSelection = (menuSelection + maxMenuSelection) % (maxMenuSelection + 1);
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Down)) {
|
||||
menuSelection = 1; // Delete
|
||||
menuSelection = (menuSelection + 1) % (maxMenuSelection + 1);
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
@@ -346,8 +366,35 @@ void MyLibraryActivity::loop() {
|
||||
ignoreNextConfirmRelease = false;
|
||||
return;
|
||||
}
|
||||
selectedAction = (menuSelection == 0) ? ActionType::Archive : ActionType::Delete;
|
||||
uiState = UIState::Confirming;
|
||||
|
||||
// Map menu selection to action type
|
||||
if (currentTab == Tab::Recent) {
|
||||
// Recent tab: Archive(0), Delete(1), Remove from Recents(2), Clear All Recents(3)
|
||||
switch (menuSelection) {
|
||||
case 0:
|
||||
selectedAction = ActionType::Archive;
|
||||
break;
|
||||
case 1:
|
||||
selectedAction = ActionType::Delete;
|
||||
break;
|
||||
case 2:
|
||||
selectedAction = ActionType::RemoveFromRecents;
|
||||
break;
|
||||
case 3:
|
||||
selectedAction = ActionType::ClearAllRecents;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Files tab: Archive(0), Delete(1)
|
||||
selectedAction = (menuSelection == 0) ? ActionType::Archive : ActionType::Delete;
|
||||
}
|
||||
|
||||
// Clear All Recents needs its own confirmation dialog
|
||||
if (selectedAction == ActionType::ClearAllRecents) {
|
||||
uiState = UIState::ClearAllRecentsConfirming;
|
||||
} else {
|
||||
uiState = UIState::Confirming;
|
||||
}
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
@@ -429,6 +476,26 @@ void MyLibraryActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle clear all recents confirmation state
|
||||
if (uiState == UIState::ClearAllRecentsConfirming) {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
uiState = UIState::ActionMenu;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
RECENT_BOOKS.clearAll();
|
||||
loadRecentBooks();
|
||||
selectorIndex = 0;
|
||||
uiState = UIState::Normal;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal state handling
|
||||
const int itemCount = getCurrentItemCount();
|
||||
const int pageItems = getPageItems();
|
||||
@@ -579,12 +646,21 @@ void MyLibraryActivity::loop() {
|
||||
}
|
||||
|
||||
void MyLibraryActivity::displayTaskLoop() {
|
||||
bool coverPreloaded = false;
|
||||
|
||||
while (true) {
|
||||
if (updateRequired) {
|
||||
updateRequired = false;
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
render();
|
||||
xSemaphoreGive(renderingMutex);
|
||||
|
||||
// After first render, pre-allocate cover buffer for Home screen
|
||||
// This happens in background so Home screen loads faster when user navigates there
|
||||
if (!coverPreloaded) {
|
||||
coverPreloaded = true;
|
||||
HomeActivity::preloadCoverBuffer();
|
||||
}
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
@@ -618,6 +694,12 @@ void MyLibraryActivity::render() const {
|
||||
return;
|
||||
}
|
||||
|
||||
if (uiState == UIState::ClearAllRecentsConfirming) {
|
||||
renderClearAllRecentsConfirmation();
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal state - draw library view
|
||||
// Draw tab bar
|
||||
std::vector<TabInfo> tabs = {{"Recent", currentTab == Tab::Recent},
|
||||
@@ -735,7 +817,7 @@ void MyLibraryActivity::renderRecentTab() const {
|
||||
}
|
||||
|
||||
// Use full width if no thumbnail, otherwise use reduced width
|
||||
const int availableWidth = hasThumb ? textMaxWidth : (pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
|
||||
const int baseAvailableWidth = hasThumb ? textMaxWidth : (pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
|
||||
|
||||
// Line 1: Title
|
||||
std::string title = book.title;
|
||||
@@ -751,12 +833,60 @@ void MyLibraryActivity::renderRecentTab() const {
|
||||
title.resize(dot);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract tags for badges (only if we'll show them - when NOT selected)
|
||||
constexpr int badgeSpacing = 4; // Gap between badges
|
||||
constexpr int badgePadding = 10; // Horizontal padding inside badge (5 each side)
|
||||
constexpr int badgeToThumbGap = 8; // Gap between rightmost badge and cover art
|
||||
int totalBadgeWidth = 0;
|
||||
BookTags tags;
|
||||
|
||||
if (!isSelected) {
|
||||
tags = StringUtils::extractBookTags(book.path);
|
||||
if (!tags.extensionTag.empty()) {
|
||||
totalBadgeWidth += renderer.getTextWidth(SMALL_FONT_ID, tags.extensionTag.c_str()) + badgePadding;
|
||||
}
|
||||
if (!tags.suffixTag.empty()) {
|
||||
if (totalBadgeWidth > 0) {
|
||||
totalBadgeWidth += badgeSpacing;
|
||||
}
|
||||
totalBadgeWidth += renderer.getTextWidth(SMALL_FONT_ID, tags.suffixTag.c_str()) + badgePadding;
|
||||
}
|
||||
}
|
||||
|
||||
// When selected, use full width (no badges shown)
|
||||
// When not selected, reserve space for badges at the right edge (plus gap to thumbnail)
|
||||
const int badgeReservedWidth = totalBadgeWidth > 0 ? (totalBadgeWidth + badgeSpacing + badgeToThumbGap) : 0;
|
||||
const int availableWidth = isSelected ? baseAvailableWidth : (baseAvailableWidth - badgeReservedWidth);
|
||||
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title.c_str(), availableWidth);
|
||||
renderer.drawText(UI_12_FONT_ID, LEFT_MARGIN, y + 2, truncatedTitle.c_str(), !isSelected);
|
||||
|
||||
// Draw badges right-aligned (near thumbnail or right edge) - only when NOT selected
|
||||
if (!isSelected && totalBadgeWidth > 0) {
|
||||
// Position badges at the right edge of the available text area (with gap to thumbnail)
|
||||
const int badgeAreaRight = LEFT_MARGIN + baseAvailableWidth - badgeToThumbGap;
|
||||
int badgeX = badgeAreaRight - totalBadgeWidth;
|
||||
|
||||
// Center badge vertically within title line height
|
||||
const int titleLineHeight = renderer.getLineHeight(UI_12_FONT_ID);
|
||||
const int badgeLineHeight = renderer.getLineHeight(SMALL_FONT_ID);
|
||||
const int badgeVerticalPadding = 4; // 2px padding top + bottom in badge
|
||||
const int badgeHeight = badgeLineHeight + badgeVerticalPadding;
|
||||
const int badgeY = y + 2 + (titleLineHeight - badgeHeight) / 2;
|
||||
|
||||
if (!tags.extensionTag.empty()) {
|
||||
int badgeWidth = ScreenComponents::drawPillBadge(renderer, badgeX, badgeY, tags.extensionTag.c_str(),
|
||||
SMALL_FONT_ID, false);
|
||||
badgeX += badgeWidth + badgeSpacing;
|
||||
}
|
||||
if (!tags.suffixTag.empty()) {
|
||||
ScreenComponents::drawPillBadge(renderer, badgeX, badgeY, tags.suffixTag.c_str(), SMALL_FONT_ID, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Line 2: Author
|
||||
if (!book.author.empty()) {
|
||||
auto truncatedAuthor = renderer.truncatedText(UI_10_FONT_ID, book.author.c_str(), availableWidth);
|
||||
auto truncatedAuthor = renderer.truncatedText(UI_10_FONT_ID, book.author.c_str(), baseAvailableWidth);
|
||||
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, y + 32, truncatedAuthor.c_str(), !isSelected);
|
||||
}
|
||||
}
|
||||
@@ -828,11 +958,13 @@ void MyLibraryActivity::renderActionMenu() const {
|
||||
auto truncatedName = renderer.truncatedText(UI_10_FONT_ID, actionTargetName.c_str(), pageWidth - 40);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, filenameY, truncatedName.c_str());
|
||||
|
||||
// Menu options
|
||||
const int menuStartY = pageHeight / 2 - 30;
|
||||
constexpr int menuLineHeight = 40;
|
||||
constexpr int menuItemWidth = 120;
|
||||
// Menu options - 4 for Recent tab, 2 for Files tab
|
||||
const bool isRecentTab = (currentTab == Tab::Recent);
|
||||
const int menuItemCount = isRecentTab ? 4 : 2;
|
||||
constexpr int menuLineHeight = 35;
|
||||
constexpr int menuItemWidth = 160;
|
||||
const int menuX = (pageWidth - menuItemWidth) / 2;
|
||||
const int menuStartY = pageHeight / 2 - (menuItemCount * menuLineHeight) / 2;
|
||||
|
||||
// Archive option
|
||||
if (menuSelection == 0) {
|
||||
@@ -846,6 +978,21 @@ void MyLibraryActivity::renderActionMenu() const {
|
||||
}
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, menuStartY + menuLineHeight, "Delete", menuSelection != 1);
|
||||
|
||||
// Recent tab only: Remove from Recents and Clear All Recents
|
||||
if (isRecentTab) {
|
||||
// Remove from Recents option
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
// Draw side button hints (up/down navigation)
|
||||
renderer.drawSideButtonHints(UI_10_FONT_ID, ">", "<");
|
||||
|
||||
@@ -859,7 +1006,21 @@ void MyLibraryActivity::renderConfirmation() const {
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
// Title based on action
|
||||
const char* actionTitle = (selectedAction == ActionType::Archive) ? "Archive Book?" : "Delete Book?";
|
||||
const char* actionTitle;
|
||||
switch (selectedAction) {
|
||||
case ActionType::Archive:
|
||||
actionTitle = "Archive Book?";
|
||||
break;
|
||||
case ActionType::Delete:
|
||||
actionTitle = "Delete Book?";
|
||||
break;
|
||||
case ActionType::RemoveFromRecents:
|
||||
actionTitle = "Remove from Recents?";
|
||||
break;
|
||||
default:
|
||||
actionTitle = "Confirm Action";
|
||||
break;
|
||||
}
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 20, actionTitle, true, EpdFontFamily::BOLD);
|
||||
|
||||
// Show filename
|
||||
@@ -872,9 +1033,12 @@ void MyLibraryActivity::renderConfirmation() const {
|
||||
if (selectedAction == ActionType::Archive) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, warningY, "Book will be moved to archive.");
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, warningY + 25, "Reading progress will be saved.");
|
||||
} else {
|
||||
} else if (selectedAction == ActionType::Delete) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, warningY, "Book will be permanently deleted!", true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, warningY + 25, "This cannot be undone.");
|
||||
} else if (selectedAction == ActionType::RemoveFromRecents) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, warningY, "Book will be removed from recents.");
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, warningY + 25, "The file will not be deleted.");
|
||||
}
|
||||
|
||||
// Draw bottom button hints
|
||||
@@ -941,3 +1105,20 @@ void MyLibraryActivity::renderListDeleteConfirmation() const {
|
||||
const auto labels = mappedInput.mapLabels("« Cancel", "Confirm", "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
}
|
||||
|
||||
void MyLibraryActivity::renderClearAllRecentsConfirmation() const {
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
// Title
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 20, "Clear All Recents?", true, EpdFontFamily::BOLD);
|
||||
|
||||
// Warning text
|
||||
const int warningY = pageHeight / 2 - 20;
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, warningY, "All books will be removed from");
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, warningY + 25, "the recent list.");
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, warningY + 60, "Book files will not be deleted.");
|
||||
|
||||
// Draw bottom button hints
|
||||
const auto labels = mappedInput.mapLabels("« Cancel", "Confirm", "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ struct ThumbExistsCache {
|
||||
class MyLibraryActivity final : public Activity {
|
||||
public:
|
||||
enum class Tab { Recent, Lists, Files };
|
||||
enum class UIState { Normal, ActionMenu, Confirming, ListActionMenu, ListConfirmingDelete };
|
||||
enum class ActionType { Archive, Delete };
|
||||
enum class UIState { Normal, ActionMenu, Confirming, ListActionMenu, ListConfirmingDelete, ClearAllRecentsConfirming };
|
||||
enum class ActionType { Archive, Delete, RemoveFromRecents, ClearAllRecents };
|
||||
|
||||
private:
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
@@ -47,6 +47,12 @@ class MyLibraryActivity final : public Activity {
|
||||
// Static thumbnail existence cache - persists across activity enter/exit
|
||||
static constexpr int MAX_THUMB_CACHE = 10;
|
||||
static ThumbExistsCache thumbExistsCache[MAX_THUMB_CACHE];
|
||||
|
||||
public:
|
||||
// Clear the thumbnail existence cache (call when disk cache is cleared)
|
||||
static void clearThumbExistsCache();
|
||||
|
||||
private:
|
||||
|
||||
// Lists tab state
|
||||
std::vector<std::string> lists;
|
||||
@@ -100,6 +106,9 @@ class MyLibraryActivity final : public Activity {
|
||||
void renderListActionMenu() const;
|
||||
void renderListDeleteConfirmation() const;
|
||||
|
||||
// Clear all recents confirmation
|
||||
void renderClearAllRecentsConfirmation() const;
|
||||
|
||||
public:
|
||||
explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void()>& onGoHome,
|
||||
|
||||
Reference in New Issue
Block a user