mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-05 15:17:37 +03:00
Merge branch 'crosspoint-reader:master' into fix/hyphenate-after-ellipsis
This commit is contained in:
commit
84208592ba
@ -1,5 +1,6 @@
|
|||||||
#include "OpdsBookBrowserActivity.h"
|
#include "OpdsBookBrowserActivity.h"
|
||||||
|
|
||||||
|
#include <Epub.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
@ -355,6 +356,12 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
|
|||||||
|
|
||||||
if (result == HttpDownloader::OK) {
|
if (result == HttpDownloader::OK) {
|
||||||
Serial.printf("[%lu] [OPDS] Download complete: %s\n", millis(), filename.c_str());
|
Serial.printf("[%lu] [OPDS] Download complete: %s\n", millis(), filename.c_str());
|
||||||
|
|
||||||
|
// Invalidate any existing cache for this file to prevent stale metadata issues
|
||||||
|
Epub epub(filename, "/.crosspoint");
|
||||||
|
epub.clearCache();
|
||||||
|
Serial.printf("[%lu] [OPDS] Cleared cache for: %s\n", millis(), filename.c_str());
|
||||||
|
|
||||||
state = BrowserState::BROWSING;
|
state = BrowserState::BROWSING;
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
#include "CalibreSettingsActivity.h"
|
#include "CalibreSettingsActivity.h"
|
||||||
|
#include "ClearCacheActivity.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "KOReaderSettingsActivity.h"
|
#include "KOReaderSettingsActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
@ -110,6 +111,14 @@ void CategorySettingsActivity::toggleCurrentSetting() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}));
|
}));
|
||||||
xSemaphoreGive(renderingMutex);
|
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, "Check for updates") == 0) {
|
} else if (strcmp(setting.name, "Check for updates") == 0) {
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
exitActivity();
|
exitActivity();
|
||||||
|
|||||||
178
src/activities/settings/ClearCacheActivity.cpp
Normal file
178
src/activities/settings/ClearCacheActivity.cpp
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
#include "ClearCacheActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <HardwareSerial.h>
|
||||||
|
#include <SDCardManager.h>
|
||||||
|
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
void ClearCacheActivity::taskTrampoline(void* param) {
|
||||||
|
auto* self = static_cast<ClearCacheActivity*>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/activities/settings/ClearCacheActivity.h
Normal file
37
src/activities/settings/ClearCacheActivity.h
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
|
||||||
|
class ClearCacheActivity final : public ActivityWithSubactivity {
|
||||||
|
public:
|
||||||
|
explicit ClearCacheActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
|
const std::function<void()>& 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<void()> goBack;
|
||||||
|
|
||||||
|
int clearedCount = 0;
|
||||||
|
int failedCount = 0;
|
||||||
|
|
||||||
|
static void taskTrampoline(void* param);
|
||||||
|
[[noreturn]] void displayTaskLoop();
|
||||||
|
void render();
|
||||||
|
void clearCache();
|
||||||
|
};
|
||||||
@ -44,11 +44,11 @@ const SettingInfo controlsSettings[controlsSettingsCount] = {
|
|||||||
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip),
|
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip),
|
||||||
SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})};
|
SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})};
|
||||||
|
|
||||||
constexpr int systemSettingsCount = 4;
|
constexpr int systemSettingsCount = 5;
|
||||||
const SettingInfo systemSettings[systemSettingsCount] = {
|
const SettingInfo systemSettings[systemSettingsCount] = {
|
||||||
SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout,
|
SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout,
|
||||||
{"1 min", "5 min", "10 min", "15 min", "30 min"}),
|
{"1 min", "5 min", "10 min", "15 min", "30 min"}),
|
||||||
SettingInfo::Action("KOReader Sync"), SettingInfo::Action("Calibre Settings"),
|
SettingInfo::Action("KOReader Sync"), SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Clear Cache"),
|
||||||
SettingInfo::Action("Check for updates")};
|
SettingInfo::Action("Check for updates")};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
#include "CrossPointWebServer.h"
|
#include "CrossPointWebServer.h"
|
||||||
|
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
|
#include <Epub.h>
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <SDCardManager.h>
|
#include <SDCardManager.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
@ -10,6 +11,7 @@
|
|||||||
|
|
||||||
#include "html/FilesPageHtml.generated.h"
|
#include "html/FilesPageHtml.generated.h"
|
||||||
#include "html/HomePageHtml.generated.h"
|
#include "html/HomePageHtml.generated.h"
|
||||||
|
#include "util/StringUtils.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Folders/files to hide from the web interface file browser
|
// Folders/files to hide from the web interface file browser
|
||||||
@ -28,6 +30,15 @@ size_t wsUploadSize = 0;
|
|||||||
size_t wsUploadReceived = 0;
|
size_t wsUploadReceived = 0;
|
||||||
unsigned long wsUploadStartTime = 0;
|
unsigned long wsUploadStartTime = 0;
|
||||||
bool wsUploadInProgress = false;
|
bool wsUploadInProgress = false;
|
||||||
|
|
||||||
|
// Helper function to clear epub cache after upload
|
||||||
|
void clearEpubCacheIfNeeded(const String& filePath) {
|
||||||
|
// Only clear cache for .epub files
|
||||||
|
if (StringUtils::checkFileExtension(filePath, ".epub")) {
|
||||||
|
Epub(filePath.c_str(), "/.crosspoint").clearCache();
|
||||||
|
Serial.printf("[%lu] [WEB] Cleared epub cache for: %s\n", millis(), filePath.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// File listing page template - now using generated headers:
|
// File listing page template - now using generated headers:
|
||||||
@ -500,6 +511,12 @@ void CrossPointWebServer::handleUpload() const {
|
|||||||
uploadFileName.c_str(), uploadSize, elapsed, avgKbps);
|
uploadFileName.c_str(), uploadSize, elapsed, avgKbps);
|
||||||
Serial.printf("[%lu] [WEB] [UPLOAD] Diagnostics: %d writes, total write time: %lu ms (%.1f%%)\n", millis(),
|
Serial.printf("[%lu] [WEB] [UPLOAD] Diagnostics: %d writes, total write time: %lu ms (%.1f%%)\n", millis(),
|
||||||
writeCount, totalWriteTime, writePercent);
|
writeCount, totalWriteTime, writePercent);
|
||||||
|
|
||||||
|
// Clear epub cache to prevent stale metadata issues when overwriting files
|
||||||
|
String filePath = uploadPath;
|
||||||
|
if (!filePath.endsWith("/")) filePath += "/";
|
||||||
|
filePath += uploadFileName;
|
||||||
|
clearEpubCacheIfNeeded(filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (upload.status == UPLOAD_FILE_ABORTED) {
|
} else if (upload.status == UPLOAD_FILE_ABORTED) {
|
||||||
@ -787,6 +804,12 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t*
|
|||||||
Serial.printf("[%lu] [WS] Upload complete: %s (%d bytes in %lu ms, %.1f KB/s)\n", millis(),
|
Serial.printf("[%lu] [WS] Upload complete: %s (%d bytes in %lu ms, %.1f KB/s)\n", millis(),
|
||||||
wsUploadFileName.c_str(), wsUploadSize, elapsed, kbps);
|
wsUploadFileName.c_str(), wsUploadSize, elapsed, kbps);
|
||||||
|
|
||||||
|
// Clear epub cache to prevent stale metadata issues when overwriting files
|
||||||
|
String filePath = wsUploadPath;
|
||||||
|
if (!filePath.endsWith("/")) filePath += "/";
|
||||||
|
filePath += wsUploadFileName;
|
||||||
|
clearEpubCacheIfNeeded(filePath);
|
||||||
|
|
||||||
wsServer->sendTXT(num, "DONE");
|
wsServer->sendTXT(num, "DONE");
|
||||||
lastProgressSent = 0;
|
lastProgressSent = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,6 +49,18 @@ bool checkFileExtension(const std::string& fileName, const char* extension) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool checkFileExtension(const String& fileName, const char* extension) {
|
||||||
|
if (fileName.length() < strlen(extension)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String localFile(fileName);
|
||||||
|
String localExtension(extension);
|
||||||
|
localFile.toLowerCase();
|
||||||
|
localExtension.toLowerCase();
|
||||||
|
return localFile.endsWith(localExtension);
|
||||||
|
}
|
||||||
|
|
||||||
size_t utf8RemoveLastChar(std::string& str) {
|
size_t utf8RemoveLastChar(std::string& str) {
|
||||||
if (str.empty()) return 0;
|
if (str.empty()) return 0;
|
||||||
size_t pos = str.size() - 1;
|
size_t pos = str.size() - 1;
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <WString.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace StringUtils {
|
namespace StringUtils {
|
||||||
@ -15,6 +17,7 @@ std::string sanitizeFilename(const std::string& name, size_t maxLength = 100);
|
|||||||
* Check if the given filename ends with the specified extension (case-insensitive).
|
* Check if the given filename ends with the specified extension (case-insensitive).
|
||||||
*/
|
*/
|
||||||
bool checkFileExtension(const std::string& fileName, const char* extension);
|
bool checkFileExtension(const std::string& fileName, const char* extension);
|
||||||
|
bool checkFileExtension(const String& fileName, const char* extension);
|
||||||
|
|
||||||
// UTF-8 safe string truncation - removes one character from the end
|
// UTF-8 safe string truncation - removes one character from the end
|
||||||
// Returns the new size after removing one UTF-8 character
|
// Returns the new size after removing one UTF-8 character
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user