#include "RecentBooksStore.h" #include #include #include #include #include #include #include #include namespace { constexpr uint8_t RECENT_BOOKS_FILE_VERSION = 3; constexpr char RECENT_BOOKS_FILE_BIN[] = "/.crosspoint/recent.bin"; constexpr char RECENT_BOOKS_FILE_JSON[] = "/.crosspoint/recent.json"; constexpr char RECENT_BOOKS_FILE_BAK[] = "/.crosspoint/recent.bin.bak"; constexpr int MAX_RECENT_BOOKS = 10; } // namespace RecentBooksStore RecentBooksStore::instance; void RecentBooksStore::addBook(const std::string& path, const std::string& title, const std::string& author, const std::string& series, const std::string& coverBmpPath) { int8_t embeddedStyleOverride = -1; int8_t imageRenderingOverride = -1; int8_t fontFamilyOverride = -1; std::string sdFontFamilyOverride; int8_t fontSizeOverride = -1; int8_t bionicReadingOverride = -1; // Remove existing entry if present auto it = std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); if (it != recentBooks.end()) { embeddedStyleOverride = it->embeddedStyleOverride; imageRenderingOverride = it->imageRenderingOverride; fontFamilyOverride = it->fontFamilyOverride; sdFontFamilyOverride = it->sdFontFamilyOverride; fontSizeOverride = it->fontSizeOverride; bionicReadingOverride = it->bionicReadingOverride; recentBooks.erase(it); } // Add to front recentBooks.insert(recentBooks.begin(), {path, title, author, series, coverBmpPath, embeddedStyleOverride, imageRenderingOverride, fontFamilyOverride, sdFontFamilyOverride, fontSizeOverride, bionicReadingOverride}); // Trim to max size if (recentBooks.size() > MAX_RECENT_BOOKS) { recentBooks.resize(MAX_RECENT_BOOKS); } saveToFile(); } void RecentBooksStore::removeBook(const std::string& path) { auto it = std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); if (it != recentBooks.end()) { recentBooks.erase(it); saveToFile(); } } void RecentBooksStore::updateBook(const std::string& path, const std::string& title, const std::string& author, const std::string& series, const std::string& coverBmpPath) { auto it = std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); if (it != recentBooks.end()) { RecentBook& book = *it; book.title = title; book.author = author; book.series = series; book.coverBmpPath = coverBmpPath; saveToFile(); } } RecentBook RecentBooksStore::getBookByPath(const std::string& path) const { auto it = std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); if (it != recentBooks.end()) { return *it; } return RecentBook{}; } bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t embeddedStyleOverride, const int8_t imageRenderingOverride) { auto it = std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); if (it == recentBooks.end()) { return false; } return setReaderOverrides(path, embeddedStyleOverride, imageRenderingOverride, it->fontFamilyOverride, it->sdFontFamilyOverride, it->fontSizeOverride, it->bionicReadingOverride); } bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t embeddedStyleOverride, const int8_t imageRenderingOverride, const int8_t fontFamilyOverride, const int8_t fontSizeOverride) { auto it = std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); if (it == recentBooks.end()) { return false; } const std::string sdOverride = (fontFamilyOverride >= 0) ? std::string() : it->sdFontFamilyOverride; return setReaderOverrides(path, embeddedStyleOverride, imageRenderingOverride, fontFamilyOverride, sdOverride, fontSizeOverride, it->bionicReadingOverride); } bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t embeddedStyleOverride, const int8_t imageRenderingOverride, const int8_t fontFamilyOverride, const std::string& sdFontFamilyOverride, const int8_t fontSizeOverride) { auto it = std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); if (it == recentBooks.end()) { return false; } return setReaderOverrides(path, embeddedStyleOverride, imageRenderingOverride, fontFamilyOverride, sdFontFamilyOverride, fontSizeOverride, it->bionicReadingOverride); } bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t embeddedStyleOverride, const int8_t imageRenderingOverride, const bool bionicReadingOverride) { auto it = std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); if (it == recentBooks.end()) { return false; } return setReaderOverrides(path, embeddedStyleOverride, imageRenderingOverride, it->fontFamilyOverride, it->sdFontFamilyOverride, it->fontSizeOverride, bionicReadingOverride); } bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t embeddedStyleOverride, const int8_t imageRenderingOverride, const int8_t fontFamilyOverride, const int8_t fontSizeOverride, const bool bionicReadingOverride) { auto it = std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); if (it == recentBooks.end()) { return false; } const std::string sdOverride = (fontFamilyOverride >= 0) ? std::string() : it->sdFontFamilyOverride; return setReaderOverrides(path, embeddedStyleOverride, imageRenderingOverride, fontFamilyOverride, sdOverride, fontSizeOverride, bionicReadingOverride); } bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t embeddedStyleOverride, const int8_t imageRenderingOverride, const int8_t fontFamilyOverride, const std::string& sdFontFamilyOverride, const int8_t fontSizeOverride, const bool bionicReadingOverride) { auto it = std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); if (it == recentBooks.end()) { return false; } it->embeddedStyleOverride = embeddedStyleOverride; it->imageRenderingOverride = imageRenderingOverride; it->fontFamilyOverride = fontFamilyOverride; it->sdFontFamilyOverride = sdFontFamilyOverride; it->fontSizeOverride = fontSizeOverride; it->bionicReadingOverride = bionicReadingOverride; return saveToFile(); } bool RecentBooksStore::saveToFile() const { Storage.mkdir("/.crosspoint"); return JsonSettingsIO::saveRecentBooks(*this, RECENT_BOOKS_FILE_JSON); } RecentBook RecentBooksStore::getDataFromBook(std::string path) const { std::string lastBookFileName = ""; const size_t lastSlash = path.find_last_of('/'); if (lastSlash != std::string::npos) { lastBookFileName = path.substr(lastSlash + 1); } LOG_DBG("RBS", "Loading recent book: %s", path.c_str()); // 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 (FsHelpers::hasEpubExtension(lastBookFileName)) { Epub epub(path, "/.crosspoint"); epub.load(false, true); std::string series = epub.getSeries(); if (!series.empty() && !epub.getSeriesIndex().empty()) series += " #" + epub.getSeriesIndex(); return RecentBook{path, epub.getTitle(), epub.getAuthor(), series, epub.getThumbBmpPath()}; } else if (FsHelpers::hasXtcExtension(lastBookFileName)) { Xtc xtc(path, "/.crosspoint"); if (xtc.load()) { return RecentBook{path, xtc.getTitle(), xtc.getAuthor(), "", xtc.getThumbBmpPath()}; } } else if (FsHelpers::hasTxtExtension(lastBookFileName) || FsHelpers::hasMarkdownExtension(lastBookFileName)) { return RecentBook{path, lastBookFileName, "", "", ""}; } return RecentBook{path, "", "", "", ""}; } bool RecentBooksStore::loadFromFile() { // Try JSON first if (Storage.exists(RECENT_BOOKS_FILE_JSON)) { String json = Storage.readFile(RECENT_BOOKS_FILE_JSON); if (!json.isEmpty()) { return JsonSettingsIO::loadRecentBooks(*this, json.c_str()); } } // Fall back to binary migration if (Storage.exists(RECENT_BOOKS_FILE_BIN)) { if (loadFromBinaryFile()) { saveToFile(); Storage.rename(RECENT_BOOKS_FILE_BIN, RECENT_BOOKS_FILE_BAK); LOG_DBG("RBS", "Migrated recent.bin to recent.json"); return true; } } return false; } bool RecentBooksStore::loadFromBinaryFile() { FsFile inputFile; if (!Storage.openFileForRead("RBS", RECENT_BOOKS_FILE_BIN, inputFile)) { return false; } uint8_t version; serialization::readPod(inputFile, version); if (version == 1 || version == 2) { // Old version, just read paths uint8_t count; serialization::readPod(inputFile, count); std::vector tmpRecentBooks; tmpRecentBooks.reserve(count); for (uint8_t i = 0; i < count; i++) { std::string path; if (!serialization::readString(inputFile, path)) { LOG_ERR("RBS", "Corrupt recent.bin: string too long at entry %u", i); inputFile.close(); return false; } // load book to get missing data RecentBook book = getDataFromBook(path); if (version == 2) { // v2 always stores title and author after path; consume them regardless // of whether live metadata was found, to keep the stream aligned. std::string storedTitle, storedAuthor; if (!serialization::readString(inputFile, storedTitle) || !serialization::readString(inputFile, storedAuthor)) { LOG_ERR("RBS", "Corrupt recent.bin: string too long at entry %u", i); inputFile.close(); return false; } // Prefer live metadata; fall back to stored when live is unavailable. const std::string& title = !book.title.empty() ? book.title : storedTitle; const std::string& author = !book.title.empty() ? book.author : storedAuthor; if (!title.empty()) { tmpRecentBooks.push_back({path, title, author, "", ""}); } } else { // v1: no stored title/author bytes if (!book.title.empty()) { tmpRecentBooks.push_back(book); } } } recentBooks = std::move(tmpRecentBooks); } else if (version == 3) { uint8_t count; serialization::readPod(inputFile, count); std::vector tmpRecentBooks; tmpRecentBooks.reserve(count); uint8_t omitted = 0; for (uint8_t i = 0; i < count; i++) { std::string path, title, author, coverBmpPath; if (!serialization::readString(inputFile, path) || !serialization::readString(inputFile, title) || !serialization::readString(inputFile, author) || !serialization::readString(inputFile, coverBmpPath)) { LOG_ERR("RBS", "Corrupt recent.bin: string too long at entry %u", i); inputFile.close(); return false; } // Omit books with missing title (e.g. saved before metadata was available) if (title.empty()) { omitted++; continue; } tmpRecentBooks.push_back({path, title, author, "", coverBmpPath}); } recentBooks = std::move(tmpRecentBooks); if (omitted > 0) { inputFile.close(); saveToFile(); LOG_DBG("RBS", "Omitted %u recent book(s) with missing title", omitted); return true; } } else { LOG_ERR("RBS", "Deserialization failed: Unknown version %u", version); inputFile.close(); return false; } inputFile.close(); LOG_DBG("RBS", "Recent books loaded from binary file (%d entries)", static_cast(recentBooks.size())); return true; }