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:
cottongin 2026-01-30 22:22:22 -05:00
parent 5464d9de3a
commit 448ce55bb4
No known key found for this signature in database
GPG Key ID: 0ECC91FE4655C262
6 changed files with 345 additions and 52 deletions

View File

@ -303,6 +303,85 @@ bool BookManager::deleteBook(const std::string& bookPath, bool isArchived) {
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> archivedBooks;

View File

@ -57,6 +57,14 @@ class BookManager {
*/
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:
// Extract filename from a full path
static std::string getFilename(const std::string& path);

View File

@ -503,22 +503,29 @@ void MyLibraryActivity::executeAction() {
} else if (selectedAction == ActionType::RemoveFromRecents) {
// Just remove from recents list, don't touch the file
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
if (success) {
// Reload data
loadRecentBooks();
if (selectedAction != ActionType::RemoveFromRecents) {
loadFiles(); // Only reload files for Archive/Delete
if (selectedAction != ActionType::RemoveFromRecents && selectedAction != ActionType::ClearCache) {
loadFiles(); // Only reload files for Archive/Delete (not needed for cache clear)
}
// Adjust selector if needed
const int itemCount = getCurrentItemCount();
if (selectorIndex >= itemCount && itemCount > 0) {
selectorIndex = itemCount - 1;
} else if (itemCount == 0) {
selectorIndex = 0;
// Adjust selector if needed (not needed for ClearCache since item count doesn't change)
if (selectedAction != ActionType::ClearCache) {
const int itemCount = getCurrentItemCount();
if (selectorIndex >= itemCount && itemCount > 0) {
selectorIndex = itemCount - 1;
} else if (itemCount == 0) {
selectorIndex = 0;
}
}
}
@ -577,8 +584,8 @@ 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;
// Menu has 5 options in Recent tab, 3 options in Files tab
const int maxMenuSelection = (currentTab == Tab::Recent) ? 4 : 2;
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
uiState = UIState::Normal;
@ -608,7 +615,7 @@ void MyLibraryActivity::loop() {
// Map menu selection to action type
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) {
case 0:
selectedAction = ActionType::Archive;
@ -617,20 +624,37 @@ void MyLibraryActivity::loop() {
selectedAction = ActionType::Delete;
break;
case 2:
selectedAction = ActionType::RemoveFromRecents;
selectedAction = ActionType::ClearCache;
break;
case 3:
selectedAction = ActionType::RemoveFromRecents;
break;
case 4:
selectedAction = ActionType::ClearAllRecents;
break;
}
} else {
// Files tab: Archive(0), Delete(1)
selectedAction = (menuSelection == 0) ? ActionType::Archive : ActionType::Delete;
// Files tab: Archive(0), Delete(1), Clear Cache(2)
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
if (selectedAction == ActionType::ClearAllRecents) {
uiState = UIState::ClearAllRecentsConfirming;
} else if (selectedAction == ActionType::ClearCache) {
// Clear Cache shows options dialog first
clearCachePreserveProgress = true; // Default to preserving progress
uiState = UIState::ClearCacheOptionsConfirming;
} else {
uiState = UIState::Confirming;
}
@ -735,6 +759,30 @@ void MyLibraryActivity::loop() {
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
const int itemCount = getCurrentItemCount();
const int pageItems = getPageItems();
@ -1303,6 +1351,12 @@ void MyLibraryActivity::render() const {
return;
}
if (uiState == UIState::ClearCacheOptionsConfirming) {
renderClearCacheOptionsConfirmation();
renderer.displayBuffer();
return;
}
// Calculate bezel-adjusted margins
const int bezelTop = renderer.getBezelOffsetTop();
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.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 int menuItemCount = isRecentTab ? 4 : 2;
const int menuItemCount = isRecentTab ? 5 : 3;
constexpr int menuLineHeight = 35;
constexpr int menuItemWidth = 160;
const int menuX = (pageWidth - menuItemWidth) / 2;
const int menuStartY = pageHeight / 2 - (menuItemCount * menuLineHeight) / 2;
// Archive option
// Archive option (index 0)
if (menuSelection == 0) {
renderer.fillRect(menuX - 10, menuStartY - 5, menuItemWidth + 20, menuLineHeight);
}
renderer.drawCenteredText(UI_10_FONT_ID, menuStartY, "Archive", menuSelection != 0);
// Delete option
// Delete option (index 1)
if (menuSelection == 1) {
renderer.fillRect(menuX - 10, menuStartY + menuLineHeight - 5, menuItemWidth + 20, menuLineHeight);
}
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
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
// Remove from Recents option (index 3)
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);
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)
@ -1828,6 +1888,54 @@ void MyLibraryActivity::renderClearAllRecentsConfirmation() const {
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 {
const auto pageWidth = renderer.getScreenWidth();
const int pageItems = getPageItems();

View File

@ -44,9 +44,10 @@ class MyLibraryActivity final : public Activity {
Confirming,
ListActionMenu,
ListConfirmingDelete,
ClearAllRecentsConfirming
ClearAllRecentsConfirming,
ClearCacheOptionsConfirming
};
enum class ActionType { Archive, Delete, RemoveFromRecents, ClearAllRecents };
enum class ActionType { Archive, Delete, RemoveFromRecents, ClearCache, ClearAllRecents };
private:
TaskHandle_t displayTaskHandle = nullptr;
@ -64,6 +65,7 @@ class MyLibraryActivity final : public Activity {
std::string actionTargetName;
int menuSelection = 0; // 0 = Archive, 1 = Delete
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
std::vector<RecentBook> recentBooks;
@ -153,6 +155,9 @@ class MyLibraryActivity final : public Activity {
// Clear all recents confirmation
void renderClearAllRecentsConfirmation() const;
// Clear cache options confirmation
void renderClearCacheOptionsConfirmation() const;
public:
explicit MyLibraryActivity(
GfxRenderer& renderer, MappedInputManager& mappedInput, const std::function<void()>& onGoHome,

View File

@ -4,6 +4,9 @@
#include <HardwareSerial.h>
#include <SDCardManager.h>
#include <string>
#include <vector>
#include "MappedInputManager.h"
#include "activities/home/HomeActivity.h"
#include "activities/home/MyLibraryActivity.h"
@ -19,6 +22,7 @@ void ClearCacheActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex();
state = WARNING;
preserveProgress = true; // Default to preserving progress
updateRequired = true;
xTaskCreate(&ClearCacheActivity::taskTrampoline, "ClearCacheActivityTask",
@ -56,6 +60,7 @@ void ClearCacheActivity::displayTaskLoop() {
}
void ClearCacheActivity::render() {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Bezel compensation
@ -67,11 +72,32 @@ void ClearCacheActivity::render() {
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, "Clear Cache", true, EpdFontFamily::BOLD);
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 - 30, "All reading progress will be lost!", true,
EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 10, "Books will need to be re-indexed", true);
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 30, "when opened again.", true);
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 70, "This will clear all cached book data.", true);
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 45, "Books will need to be re-indexed.", true);
// Preserve progress option
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", "", "");
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() {
Serial.printf("[%lu] [CLEAR_CACHE] Clearing cache...\n", millis());
Serial.printf("[%lu] [CLEAR_CACHE] Clearing cache (preserveProgress=%d)...\n", millis(), preserveProgress);
// Open .crosspoint directory
auto root = SdMan.open("/.crosspoint");
@ -127,35 +213,31 @@ void ClearCacheActivity::clearCache() {
failedCount = 0;
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()) {
file.getName(name, sizeof(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_"))) {
String fullPath = "/.crosspoint/" + itemName;
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();
cacheDirs.push_back("/.crosspoint/" + std::string(name));
}
file.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
HomeActivity::freeCoverBufferIfAllocated();
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;
updateRequired = true;
@ -163,8 +245,17 @@ void ClearCacheActivity::clearCache() {
void ClearCacheActivity::loop() {
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)) {
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);
state = CLEARING;
xSemaphoreGive(renderingMutex);

View File

@ -29,9 +29,11 @@ class ClearCacheActivity final : public ActivityWithSubactivity {
int clearedCount = 0;
int failedCount = 0;
bool preserveProgress = true; // Whether to keep progress.bin and bookmarks.bin
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render();
void clearCache();
void clearCacheDirectory(const char* dirPath); // Helper to clear a single book's cache
};