diff --git a/src/activities/settings/ClearCacheActivity.cpp b/src/activities/settings/ClearCacheActivity.cpp new file mode 100644 index 0000000..1e10c14 --- /dev/null +++ b/src/activities/settings/ClearCacheActivity.cpp @@ -0,0 +1,178 @@ +#include "ClearCacheActivity.h" + +#include +#include +#include + +#include "MappedInputManager.h" +#include "fontIds.h" + +void ClearCacheActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void ClearCacheActivity::onEnter() { + ActivityWithSubactivity::onEnter(); + + renderingMutex = xSemaphoreCreateMutex(); + state = WARNING; + updateRequired = true; + + xTaskCreate(&ClearCacheActivity::taskTrampoline, "ClearCacheActivityTask", + 4096, // Stack size + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle + ); +} + +void ClearCacheActivity::onExit() { + ActivityWithSubactivity::onExit(); + + // Wait until not rendering to delete task to avoid killing mid-instruction to EPD + xSemaphoreTake(renderingMutex, portMAX_DELAY); + if (displayTaskHandle) { + vTaskDelete(displayTaskHandle); + displayTaskHandle = nullptr; + } + vSemaphoreDelete(renderingMutex); + renderingMutex = nullptr; +} + +void ClearCacheActivity::displayTaskLoop() { + while (true) { + if (updateRequired) { + updateRequired = false; + xSemaphoreTake(renderingMutex, portMAX_DELAY); + render(); + xSemaphoreGive(renderingMutex); + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void ClearCacheActivity::render() { + const auto pageHeight = renderer.getScreenHeight(); + + renderer.clearScreen(); + renderer.drawCenteredText(UI_12_FONT_ID, 15, "Clear Cache", true, EpdFontFamily::BOLD); + + if (state == WARNING) { + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 60, "This will clear all cached book data.", true); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 30, "All reading progress will be lost!", true, + EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, "Books will need to be re-indexed", true); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 30, "when opened again.", true); + + const auto labels = mappedInput.mapLabels("« Cancel", "Clear", "", ""); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + renderer.displayBuffer(); + return; + } + + if (state == CLEARING) { + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Clearing cache...", true, EpdFontFamily::BOLD); + renderer.displayBuffer(); + return; + } + + if (state == SUCCESS) { + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, "Cache Cleared", true, EpdFontFamily::BOLD); + String resultText = String(clearedCount) + " items removed"; + if (failedCount > 0) { + resultText += ", " + String(failedCount) + " failed"; + } + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, resultText.c_str()); + + const auto labels = mappedInput.mapLabels("« Back", "", "", ""); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + renderer.displayBuffer(); + return; + } + + if (state == FAILED) { + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, "Failed to clear cache", true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, "Check serial output for details"); + + const auto labels = mappedInput.mapLabels("« Back", "", "", ""); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + renderer.displayBuffer(); + return; + } +} + +void ClearCacheActivity::clearCache() { + Serial.printf("[%lu] [CLEAR_CACHE] Clearing cache...\n", millis()); + + // Open .crosspoint directory + auto root = SdMan.open("/.crosspoint"); + if (!root || !root.isDirectory()) { + Serial.printf("[%lu] [CLEAR_CACHE] Failed to open cache directory\n", millis()); + if (root) root.close(); + state = FAILED; + updateRequired = true; + return; + } + + clearedCount = 0; + failedCount = 0; + char name[128]; + + // Iterate through all entries in the directory + for (auto file = root.openNextFile(); file; file = root.openNextFile()) { + 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_"))) { + 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(); + } + } + root.close(); + + Serial.printf("[%lu] [CLEAR_CACHE] Cache cleared: %d removed, %d failed\n", millis(), clearedCount, failedCount); + + state = SUCCESS; + updateRequired = true; +} + +void ClearCacheActivity::loop() { + if (state == WARNING) { + if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { + Serial.printf("[%lu] [CLEAR_CACHE] User confirmed, starting cache clear\n", millis()); + xSemaphoreTake(renderingMutex, portMAX_DELAY); + state = CLEARING; + xSemaphoreGive(renderingMutex); + updateRequired = true; + vTaskDelay(10 / portTICK_PERIOD_MS); + + clearCache(); + } + + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + Serial.printf("[%lu] [CLEAR_CACHE] User cancelled\n", millis()); + goBack(); + } + return; + } + + if (state == SUCCESS || state == FAILED) { + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + goBack(); + } + return; + } +} diff --git a/src/activities/settings/ClearCacheActivity.h b/src/activities/settings/ClearCacheActivity.h new file mode 100644 index 0000000..31795a9 --- /dev/null +++ b/src/activities/settings/ClearCacheActivity.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +#include + +#include "activities/ActivityWithSubactivity.h" + +class ClearCacheActivity final : public ActivityWithSubactivity { + public: + explicit ClearCacheActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& goBack) + : ActivityWithSubactivity("ClearCache", renderer, mappedInput), goBack(goBack) {} + + void onEnter() override; + void onExit() override; + void loop() override; + + private: + enum State { WARNING, CLEARING, SUCCESS, FAILED }; + + State state = WARNING; + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + bool updateRequired = false; + const std::function goBack; + + int clearedCount = 0; + int failedCount = 0; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void render(); + void clearCache(); +}; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 7907e50..0a58d81 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -6,6 +6,7 @@ #include #include "CalibreSettingsActivity.h" +#include "ClearCacheActivity.h" #include "CrossPointSettings.h" #include "KOReaderSettingsActivity.h" #include "MappedInputManager.h" @@ -14,7 +15,7 @@ // Define the static settings list namespace { -constexpr int settingsCount = 22; +constexpr int settingsCount = 23; const SettingInfo settingsList[settingsCount] = { // Should match with SLEEP_SCREEN_MODE SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}), @@ -44,6 +45,7 @@ const SettingInfo settingsList[settingsCount] = { SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}), SettingInfo::Action("KOReader Sync"), + SettingInfo::Action("Clear Cache"), SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Check for updates")}; } // namespace @@ -149,6 +151,14 @@ void SettingsActivity::toggleCurrentSetting() { updateRequired = true; })); xSemaphoreGive(renderingMutex); + } else if (strcmp(setting.name, "Clear Cache") == 0) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + exitActivity(); + enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] { + exitActivity(); + updateRequired = true; + })); + xSemaphoreGive(renderingMutex); } else if (strcmp(setting.name, "Calibre Settings") == 0) { xSemaphoreTake(renderingMutex, portMAX_DELAY); exitActivity();