Merge branch 'master' of https://github.com/crosspoint-reader/crosspoint-reader into feat-page-overlay

This commit is contained in:
jpirnay
2026-03-07 10:22:18 +01:00
41 changed files with 924 additions and 193 deletions
+4 -7
View File
@@ -1,6 +1,7 @@
#include "RecentBooksStore.h"
#include <Epub.h>
#include <FsHelpers.h>
#include <HalStorage.h>
#include <JsonSettingsIO.h>
#include <Logging.h>
@@ -9,8 +10,6 @@
#include <algorithm>
#include "util/StringUtils.h"
namespace {
constexpr uint8_t RECENT_BOOKS_FILE_VERSION = 3;
constexpr char RECENT_BOOKS_FILE_BIN[] = "/.crosspoint/recent.bin";
@@ -71,19 +70,17 @@ RecentBook RecentBooksStore::getDataFromBook(std::string path) const {
// If epub, try to load the metadata for title/author and cover.
// Use buildIfMissing=false to avoid heavy epub loading on boot; getTitle()/getAuthor() may be
// blank until the book is opened, and entries with missing title are omitted from recent list.
if (StringUtils::checkFileExtension(lastBookFileName, ".epub")) {
if (FsHelpers::hasEpubExtension(lastBookFileName)) {
Epub epub(path, "/.crosspoint");
epub.load(false, true);
return RecentBook{path, epub.getTitle(), epub.getAuthor(), epub.getThumbBmpPath()};
} else if (StringUtils::checkFileExtension(lastBookFileName, ".xtch") ||
StringUtils::checkFileExtension(lastBookFileName, ".xtc")) {
} else if (FsHelpers::hasXtcExtension(lastBookFileName)) {
// Handle XTC file
Xtc xtc(path, "/.crosspoint");
if (xtc.load()) {
return RecentBook{path, xtc.getTitle(), xtc.getAuthor(), xtc.getThumbBmpPath()};
}
} else if (StringUtils::checkFileExtension(lastBookFileName, ".txt") ||
StringUtils::checkFileExtension(lastBookFileName, ".md")) {
} else if (FsHelpers::hasTxtExtension(lastBookFileName) || FsHelpers::hasMarkdownExtension(lastBookFileName)) {
return RecentBook{path, lastBookFileName, "", ""};
}
return RecentBook{path, "", "", ""};
+5 -6
View File
@@ -1,6 +1,7 @@
#include "SleepActivity.h"
#include <Epub.h>
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h>
@@ -18,7 +19,6 @@
#include "components/UITheme.h"
#include "fontIds.h"
#include "images/Logo120.h"
#include "util/StringUtils.h"
namespace {
@@ -211,7 +211,7 @@ void SleepActivity::renderCustomSleepScreen() const {
continue;
}
if (filename.substr(filename.length() - 4) != ".bmp") {
if (!FsHelpers::hasBmpExtension(filename)) {
LOG_DBG("SLP", "Skipping non-.bmp file name: %s", name);
file.close();
continue;
@@ -378,8 +378,7 @@ void SleepActivity::renderCoverSleepScreen() const {
bool cropped = SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP;
// Check if the current book is XTC, TXT, or EPUB
if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtc") ||
StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtch")) {
if (FsHelpers::hasXtcExtension(APP_STATE.openEpubPath)) {
// Handle XTC file
Xtc lastXtc(APP_STATE.openEpubPath, "/.crosspoint");
if (!lastXtc.load()) {
@@ -393,7 +392,7 @@ void SleepActivity::renderCoverSleepScreen() const {
}
coverBmpPath = lastXtc.getCoverBmpPath();
} else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".txt")) {
} else if (FsHelpers::hasTxtExtension(APP_STATE.openEpubPath)) {
// Handle TXT file - looks for cover image in the same folder
Txt lastTxt(APP_STATE.openEpubPath, "/.crosspoint");
if (!lastTxt.load()) {
@@ -407,7 +406,7 @@ void SleepActivity::renderCoverSleepScreen() const {
}
coverBmpPath = lastTxt.getCoverBmpPath();
} else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".epub")) {
} else if (FsHelpers::hasEpubExtension(APP_STATE.openEpubPath)) {
// Handle EPUB file
Epub lastEpub(APP_STATE.openEpubPath, "/.crosspoint");
// Skip loading css since we only need metadata here
+6 -6
View File
@@ -1,6 +1,7 @@
#include "FileBrowserActivity.h"
#include <Epub.h>
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h>
@@ -11,7 +12,6 @@
#include "MappedInputManager.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "util/StringUtils.h"
namespace {
constexpr unsigned long GO_HOME_MS = 1000;
@@ -91,10 +91,10 @@ void FileBrowserActivity::loadFiles() {
if (file.isDirectory()) {
files.emplace_back(std::string(name) + "/");
} else {
auto filename = std::string(name);
if (StringUtils::checkFileExtension(filename, ".epub") || StringUtils::checkFileExtension(filename, ".xtch") ||
StringUtils::checkFileExtension(filename, ".xtc") || StringUtils::checkFileExtension(filename, ".txt") ||
StringUtils::checkFileExtension(filename, ".md") || StringUtils::checkFileExtension(filename, ".bmp")) {
std::string_view filename{name};
if (FsHelpers::hasEpubExtension(filename) || FsHelpers::hasXtcExtension(filename) ||
FsHelpers::hasTxtExtension(filename) || FsHelpers::hasMarkdownExtension(filename) ||
FsHelpers::hasBmpExtension(filename)) {
files.emplace_back(filename);
}
}
@@ -120,7 +120,7 @@ void FileBrowserActivity::onExit() {
void FileBrowserActivity::clearFileMetadata(const std::string& fullPath) {
// Only clear cache for .epub files
if (StringUtils::checkFileExtension(fullPath, ".epub")) {
if (FsHelpers::hasEpubExtension(fullPath)) {
Epub(fullPath, "/.crosspoint").clearCache();
LOG_DBG("FileBrowser", "Cleared metadata cache for: %s", fullPath.c_str());
}
+3 -4
View File
@@ -2,6 +2,7 @@
#include <Bitmap.h>
#include <Epub.h>
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h>
@@ -17,7 +18,6 @@
#include "RecentBooksStore.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "util/StringUtils.h"
int HomeActivity::getMenuItemCount() const {
int count = 4; // File Browser, Recents, File transfer, Settings
@@ -61,7 +61,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
std::string coverPath = UITheme::getCoverThumbPath(book.coverBmpPath, coverHeight);
if (!Storage.exists(coverPath.c_str())) {
// If epub, try to load the metadata for title/author and cover
if (StringUtils::checkFileExtension(book.path, ".epub")) {
if (FsHelpers::hasEpubExtension(book.path)) {
Epub epub(book.path, "/.crosspoint");
// Skip loading css since we only need metadata here
epub.load(false, true);
@@ -79,8 +79,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
}
coverRendered = false;
requestUpdate();
} else if (StringUtils::checkFileExtension(book.path, ".xtch") ||
StringUtils::checkFileExtension(book.path, ".xtc")) {
} else if (FsHelpers::hasXtcExtension(book.path)) {
// Handle XTC file
Xtc xtc(book.path, "/.crosspoint");
if (xtc.load()) {
@@ -10,7 +10,6 @@
#include "RecentBooksStore.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "util/StringUtils.h"
namespace {
constexpr unsigned long GO_HOME_MS = 1000;
+18 -1
View File
@@ -597,6 +597,16 @@ void EpubReaderActivity::render(RenderLock&& lock) {
section->currentPage = nextPageNumber;
}
if (!pendingAnchor.empty()) {
if (const auto page = section->getPageForAnchor(pendingAnchor)) {
section->currentPage = *page;
LOG_DBG("ERS", "Resolved anchor '%s' to page %d", pendingAnchor.c_str(), *page);
} else {
LOG_DBG("ERS", "Anchor '%s' not found in section %d", pendingAnchor.c_str(), currentSpineIndex);
}
pendingAnchor.clear();
}
// handles changes in reader settings and reset to approximate position based on cached progress
if (cachedChapterTotalPageCount > 0) {
// only goes to relative position if spine index matches cached value
@@ -792,12 +802,18 @@ void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool s
LOG_DBG("ERS", "Saved position [%d]: spine %d, page %d", footnoteDepth, currentSpineIndex, section->currentPage);
}
// Extract fragment anchor (e.g. "#note1" or "chapter2.xhtml#note1")
std::string anchor;
const auto hashPos = hrefStr.find('#');
if (hashPos != std::string::npos && hashPos + 1 < hrefStr.size()) {
anchor = hrefStr.substr(hashPos + 1);
}
// Check for same-file anchor reference (#anchor only)
bool sameFile = !hrefStr.empty() && hrefStr[0] == '#';
int targetSpineIndex;
if (sameFile) {
// Same file — navigate to page 0 of current spine item
targetSpineIndex = currentSpineIndex;
} else {
targetSpineIndex = epub->resolveHrefToSpineIndex(hrefStr);
@@ -811,6 +827,7 @@ void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool s
{
RenderLock lock(*this);
pendingAnchor = std::move(anchor);
currentSpineIndex = targetSpineIndex;
nextPageNumber = 0;
section.reset();
@@ -11,6 +11,9 @@ class EpubReaderActivity final : public Activity {
std::unique_ptr<Section> section = nullptr;
int currentSpineIndex = 0;
int nextPageNumber = 0;
// Set when navigating to a footnote href with a fragment (e.g. #note1).
// Cleared on the next render after the new section loads and resolves it to a page.
std::string pendingAnchor;
int pagesUntilFullRefresh = 0;
int cachedSpineIndex = 0;
int cachedChapterTotalPageCount = 0;
+5 -7
View File
@@ -1,5 +1,6 @@
#include "ReaderActivity.h"
#include <FsHelpers.h>
#include <HalStorage.h>
#include "CrossPointSettings.h"
@@ -11,7 +12,6 @@
#include "XtcReaderActivity.h"
#include "activities/util/BmpViewerActivity.h"
#include "activities/util/FullScreenMessageActivity.h"
#include "util/StringUtils.h"
std::string ReaderActivity::extractFolderPath(const std::string& filePath) {
const auto lastSlash = filePath.find_last_of('/');
@@ -21,16 +21,14 @@ std::string ReaderActivity::extractFolderPath(const std::string& filePath) {
return filePath.substr(0, lastSlash);
}
bool ReaderActivity::isXtcFile(const std::string& path) {
return StringUtils::checkFileExtension(path, ".xtc") || StringUtils::checkFileExtension(path, ".xtch");
}
bool ReaderActivity::isXtcFile(const std::string& path) { return FsHelpers::hasXtcExtension(path); }
bool ReaderActivity::isTxtFile(const std::string& path) {
return StringUtils::checkFileExtension(path, ".txt") ||
StringUtils::checkFileExtension(path, ".md"); // Treat .md as txt files (until we have a markdown reader)
return FsHelpers::hasTxtExtension(path) ||
FsHelpers::hasMarkdownExtension(path); // Treat .md as txt files (until we have a markdown reader)
}
bool ReaderActivity::isBmpFile(const std::string& path) { return StringUtils::checkFileExtension(path, ".bmp"); }
bool ReaderActivity::isBmpFile(const std::string& path) { return FsHelpers::hasBmpExtension(path); }
std::unique_ptr<Epub> ReaderActivity::loadEpub(const std::string& path) {
if (!Storage.exists(path.c_str())) {
+5 -6
View File
@@ -1,5 +1,6 @@
#include "UITheme.h"
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <Logging.h>
@@ -10,7 +11,6 @@
#include "components/themes/BaseTheme.h"
#include "components/themes/lyra/Lyra3CoversTheme.h"
#include "components/themes/lyra/LyraTheme.h"
#include "util/StringUtils.h"
namespace {
constexpr int SKIP_PAGE_MS = 700;
@@ -74,18 +74,17 @@ std::string UITheme::getCoverThumbPath(std::string coverBmpPath, int coverHeight
return coverBmpPath;
}
UIIcon UITheme::getFileIcon(std::string filename) {
UIIcon UITheme::getFileIcon(const std::string& filename) {
if (filename.back() == '/') {
return Folder;
}
if (StringUtils::checkFileExtension(filename, ".epub") || StringUtils::checkFileExtension(filename, ".xtch") ||
StringUtils::checkFileExtension(filename, ".xtc")) {
if (FsHelpers::hasEpubExtension(filename) || FsHelpers::hasXtcExtension(filename)) {
return Book;
}
if (StringUtils::checkFileExtension(filename, ".txt") || StringUtils::checkFileExtension(filename, ".md")) {
if (FsHelpers::hasTxtExtension(filename) || FsHelpers::hasMarkdownExtension(filename)) {
return Text;
}
if (StringUtils::checkFileExtension(filename, ".bmp")) {
if (FsHelpers::hasBmpExtension(filename)) {
return Image;
}
return File;
+1 -1
View File
@@ -21,7 +21,7 @@ class UITheme {
static int getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints,
bool hasSubtitle);
static std::string getCoverThumbPath(std::string coverBmpPath, int coverHeight);
static UIIcon getFileIcon(std::string filename);
static UIIcon getFileIcon(const std::string& filename);
static int getStatusBarHeight();
static int getProgressBarHeight();
+5
View File
@@ -6,6 +6,7 @@
#include <HalGPIO.h>
#include <HalPowerManager.h>
#include <HalStorage.h>
#include <HalSystem.h>
#include <I18n.h>
#include <Logging.h>
#include <SPI.h>
@@ -227,6 +228,7 @@ void setupDisplayAndFonts() {
void setup() {
t1 = millis();
HalSystem::begin();
gpio.begin();
powerManager.begin();
@@ -249,6 +251,9 @@ void setup() {
return;
}
HalSystem::checkPanic();
HalSystem::clearPanic(); // TODO: move this to an activity when we have one to display the panic info
SETTINGS.loadFromFile();
I18N.loadSettings();
KOREADER_STORE.loadFromFile();
+2 -7
View File
@@ -16,7 +16,6 @@
#include "html/FilesPageHtml.generated.h"
#include "html/HomePageHtml.generated.h"
#include "html/SettingsPageHtml.generated.h"
#include "util/StringUtils.h"
namespace {
// Folders/files to hide from the web interface file browser
@@ -44,7 +43,7 @@ unsigned long wsLastCompleteAt = 0;
// Helper function to clear epub cache after upload
void clearEpubCacheIfNeeded(const String& filePath) {
// Only clear cache for .epub files
if (StringUtils::checkFileExtension(filePath, ".epub")) {
if (FsHelpers::hasEpubExtension(filePath)) {
Epub(filePath.c_str(), "/.crosspoint").clearCache();
LOG_DBG("WEB", "Cleared epub cache for: %s", filePath.c_str());
}
@@ -391,11 +390,7 @@ void CrossPointWebServer::scanFiles(const char* path, const std::function<void(F
root.close();
}
bool CrossPointWebServer::isEpubFile(const String& filename) const {
String lower = filename;
lower.toLowerCase();
return lower.endsWith(".epub");
}
bool CrossPointWebServer::isEpubFile(const String& filename) const { return FsHelpers::hasEpubExtension(filename); }
void CrossPointWebServer::handleFileList() const {
sendHtmlContent(server.get(), FilesPageHtml, sizeof(FilesPageHtml));
+15 -19
View File
@@ -6,8 +6,6 @@
#include <Logging.h>
#include <esp_task_wdt.h>
#include "util/StringUtils.h"
namespace {
const char* HIDDEN_ITEMS[] = {"System Volume Information", "XTCache"};
constexpr size_t HIDDEN_ITEMS_COUNT = sizeof(HIDDEN_ITEMS) / sizeof(HIDDEN_ITEMS[0]);
@@ -801,28 +799,26 @@ bool WebDAVHandler::getOverwrite(WebServer& s) const {
}
void WebDAVHandler::clearEpubCacheIfNeeded(const String& path) const {
if (StringUtils::checkFileExtension(path, ".epub")) {
if (FsHelpers::hasEpubExtension(path)) {
Epub(path.c_str(), "/.crosspoint").clearCache();
LOG_DBG("DAV", "Cleared epub cache for: %s", path.c_str());
}
}
String WebDAVHandler::getMimeType(const String& path) const {
if (StringUtils::checkFileExtension(path, ".epub")) return "application/epub+zip";
if (StringUtils::checkFileExtension(path, ".pdf")) return "application/pdf";
if (StringUtils::checkFileExtension(path, ".txt")) return "text/plain";
if (StringUtils::checkFileExtension(path, ".html") || StringUtils::checkFileExtension(path, ".htm"))
return "text/html";
if (StringUtils::checkFileExtension(path, ".css")) return "text/css";
if (StringUtils::checkFileExtension(path, ".js")) return "application/javascript";
if (StringUtils::checkFileExtension(path, ".json")) return "application/json";
if (StringUtils::checkFileExtension(path, ".xml")) return "application/xml";
if (StringUtils::checkFileExtension(path, ".jpg") || StringUtils::checkFileExtension(path, ".jpeg"))
return "image/jpeg";
if (StringUtils::checkFileExtension(path, ".png")) return "image/png";
if (StringUtils::checkFileExtension(path, ".gif")) return "image/gif";
if (StringUtils::checkFileExtension(path, ".svg")) return "image/svg+xml";
if (StringUtils::checkFileExtension(path, ".zip")) return "application/zip";
if (StringUtils::checkFileExtension(path, ".gz")) return "application/gzip";
if (FsHelpers::hasEpubExtension(path)) return "application/epub+zip";
if (FsHelpers::checkFileExtension(path, ".pdf")) return "application/pdf";
if (FsHelpers::hasTxtExtension(path)) return "text/plain";
if (FsHelpers::checkFileExtension(path, ".html") || FsHelpers::checkFileExtension(path, ".htm")) return "text/html";
if (FsHelpers::checkFileExtension(path, ".css")) return "text/css";
if (FsHelpers::checkFileExtension(path, ".js")) return "application/javascript";
if (FsHelpers::checkFileExtension(path, ".json")) return "application/json";
if (FsHelpers::checkFileExtension(path, ".xml")) return "application/xml";
if (FsHelpers::hasJpgExtension(path)) return "image/jpeg";
if (FsHelpers::hasPngExtension(path)) return "image/png";
if (FsHelpers::hasGifExtension(path)) return "image/gif";
if (FsHelpers::checkFileExtension(path, ".svg")) return "image/svg+xml";
if (FsHelpers::checkFileExtension(path, ".zip")) return "application/zip";
if (FsHelpers::checkFileExtension(path, ".gz")) return "application/gzip";
return "application/octet-stream";
}
-28
View File
@@ -2,8 +2,6 @@
#include <Utf8.h>
#include <cstring>
namespace StringUtils {
std::string sanitizeFilename(const std::string& name, size_t maxBytes) {
@@ -45,30 +43,4 @@ std::string sanitizeFilename(const std::string& name, size_t maxBytes) {
return result.empty() ? "book" : result;
}
bool checkFileExtension(const std::string& fileName, const char* extension) {
if (fileName.length() < strlen(extension)) {
return false;
}
const std::string fileExt = fileName.substr(fileName.length() - strlen(extension));
for (size_t i = 0; i < fileExt.length(); i++) {
if (tolower(fileExt[i]) != tolower(extension[i])) {
return false;
}
}
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);
}
} // namespace StringUtils
-8
View File
@@ -1,7 +1,5 @@
#pragma once
#include <WString.h>
#include <string>
namespace StringUtils {
@@ -13,10 +11,4 @@ namespace StringUtils {
*/
std::string sanitizeFilename(const std::string& name, size_t maxBytes = 100);
/**
* Check if the given filename ends with the specified extension (case-insensitive).
*/
bool checkFileExtension(const std::string& fileName, const char* extension);
bool checkFileExtension(const String& fileName, const char* extension);
} // namespace StringUtils