#include "BookmarkStore.h" #include #include #include #include #include #include "util/StringUtils.h" // Include the BookmarkedBook struct definition #include "activities/home/MyLibraryActivity.h" namespace { constexpr uint8_t BOOKMARKS_FILE_VERSION = 1; constexpr char BOOKMARKS_FILENAME[] = "bookmarks.bin"; constexpr int MAX_BOOKMARKS_PER_BOOK = 100; // Get cache directory path for a book (same logic as BookManager) std::string getCacheDir(const std::string& bookPath) { const size_t hash = std::hash{}(bookPath); if (StringUtils::checkFileExtension(bookPath, ".epub")) { return "/.crosspoint/epub_" + std::to_string(hash); } else if (StringUtils::checkFileExtension(bookPath, ".txt") || StringUtils::checkFileExtension(bookPath, ".TXT") || StringUtils::checkFileExtension(bookPath, ".md")) { return "/.crosspoint/txt_" + std::to_string(hash); } return ""; } } // namespace std::string BookmarkStore::getBookmarksFilePath(const std::string& bookPath) { const std::string cacheDir = getCacheDir(bookPath); if (cacheDir.empty()) return ""; return cacheDir + "/" + BOOKMARKS_FILENAME; } std::vector BookmarkStore::getBookmarks(const std::string& bookPath) { std::vector bookmarks; loadBookmarks(bookPath, bookmarks); return bookmarks; } bool BookmarkStore::addBookmark(const std::string& bookPath, const Bookmark& bookmark) { std::vector bookmarks; loadBookmarks(bookPath, bookmarks); // Check if bookmark already exists at this location auto it = std::find_if(bookmarks.begin(), bookmarks.end(), [&](const Bookmark& b) { return b.spineIndex == bookmark.spineIndex && b.contentOffset == bookmark.contentOffset; }); if (it != bookmarks.end()) { Serial.printf("[%lu] [BMS] Bookmark already exists at spine %u, offset %u\n", millis(), bookmark.spineIndex, bookmark.contentOffset); return false; } // Add new bookmark bookmarks.push_back(bookmark); // Trim to max size (remove oldest) if (bookmarks.size() > MAX_BOOKMARKS_PER_BOOK) { // Sort by timestamp and remove oldest std::sort(bookmarks.begin(), bookmarks.end(), [](const Bookmark& a, const Bookmark& b) { return a.timestamp > b.timestamp; // Newest first }); bookmarks.resize(MAX_BOOKMARKS_PER_BOOK); } return saveBookmarks(bookPath, bookmarks); } bool BookmarkStore::removeBookmark(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset) { std::vector bookmarks; loadBookmarks(bookPath, bookmarks); auto it = std::find_if(bookmarks.begin(), bookmarks.end(), [&](const Bookmark& b) { return b.spineIndex == spineIndex && b.contentOffset == contentOffset; }); if (it == bookmarks.end()) { return false; } bookmarks.erase(it); Serial.printf("[%lu] [BMS] Removed bookmark at spine %u, offset %u\n", millis(), spineIndex, contentOffset); return saveBookmarks(bookPath, bookmarks); } bool BookmarkStore::isPageBookmarked(const std::string& bookPath, uint16_t spineIndex, uint32_t contentOffset) { std::vector bookmarks; loadBookmarks(bookPath, bookmarks); return std::any_of(bookmarks.begin(), bookmarks.end(), [&](const Bookmark& b) { return b.spineIndex == spineIndex && b.contentOffset == contentOffset; }); } int BookmarkStore::getBookmarkCount(const std::string& bookPath) { const std::string filePath = getBookmarksFilePath(bookPath); if (filePath.empty()) return 0; FsFile inputFile; if (!SdMan.openFileForRead("BMS", filePath, inputFile)) { return 0; } uint8_t version; serialization::readPod(inputFile, version); if (version != BOOKMARKS_FILE_VERSION) { inputFile.close(); return 0; } uint8_t count; serialization::readPod(inputFile, count); inputFile.close(); return count; } std::vector BookmarkStore::getBooksWithBookmarks() { std::vector result; // Scan /.crosspoint/ directory for cache folders with bookmarks auto crosspoint = SdMan.open("/.crosspoint"); if (!crosspoint || !crosspoint.isDirectory()) { if (crosspoint) crosspoint.close(); return result; } crosspoint.rewindDirectory(); char name[256]; for (auto entry = crosspoint.openNextFile(); entry; entry = crosspoint.openNextFile()) { entry.getName(name, sizeof(name)); if (!entry.isDirectory()) { entry.close(); continue; } // Check if this directory has a bookmarks file std::string dirPath = "/.crosspoint/"; dirPath += name; std::string bookmarksPath = dirPath + "/" + BOOKMARKS_FILENAME; if (SdMan.exists(bookmarksPath.c_str())) { // Read the bookmarks file to get count and book info FsFile bookmarksFile; if (SdMan.openFileForRead("BMS", bookmarksPath, bookmarksFile)) { uint8_t version; serialization::readPod(bookmarksFile, version); if (version == BOOKMARKS_FILE_VERSION) { uint8_t count; serialization::readPod(bookmarksFile, count); // Read book metadata (stored at end of file) std::string bookPath, bookTitle, bookAuthor; // Skip bookmark entries to get to metadata for (uint8_t i = 0; i < count; i++) { std::string tempName; uint16_t tempSpine; uint32_t tempOffset, tempTimestamp; uint16_t tempPage; serialization::readString(bookmarksFile, tempName); serialization::readPod(bookmarksFile, tempSpine); serialization::readPod(bookmarksFile, tempOffset); serialization::readPod(bookmarksFile, tempPage); serialization::readPod(bookmarksFile, tempTimestamp); } // Read book metadata serialization::readString(bookmarksFile, bookPath); serialization::readString(bookmarksFile, bookTitle); serialization::readString(bookmarksFile, bookAuthor); if (!bookPath.empty() && count > 0) { BookmarkedBook book; book.path = bookPath; book.title = bookTitle; book.author = bookAuthor; book.bookmarkCount = count; result.push_back(book); } } bookmarksFile.close(); } } entry.close(); } crosspoint.close(); // Sort by title std::sort(result.begin(), result.end(), [](const BookmarkedBook& a, const BookmarkedBook& b) { return a.title < b.title; }); return result; } void BookmarkStore::clearBookmarks(const std::string& bookPath) { const std::string filePath = getBookmarksFilePath(bookPath); if (filePath.empty()) return; SdMan.remove(filePath.c_str()); Serial.printf("[%lu] [BMS] Cleared all bookmarks for %s\n", millis(), bookPath.c_str()); } bool BookmarkStore::saveBookmarks(const std::string& bookPath, const std::vector& bookmarks) { const std::string cacheDir = getCacheDir(bookPath); if (cacheDir.empty()) return false; // Make sure the directory exists SdMan.mkdir(cacheDir.c_str()); const std::string filePath = cacheDir + "/" + BOOKMARKS_FILENAME; FsFile outputFile; if (!SdMan.openFileForWrite("BMS", filePath, outputFile)) { return false; } serialization::writePod(outputFile, BOOKMARKS_FILE_VERSION); const uint8_t count = static_cast(std::min(bookmarks.size(), static_cast(255))); serialization::writePod(outputFile, count); for (size_t i = 0; i < count; i++) { const auto& bookmark = bookmarks[i]; serialization::writeString(outputFile, bookmark.name); serialization::writePod(outputFile, bookmark.spineIndex); serialization::writePod(outputFile, bookmark.contentOffset); serialization::writePod(outputFile, bookmark.pageNumber); serialization::writePod(outputFile, bookmark.timestamp); } // Store book metadata at end (for getBooksWithBookmarks to read) // Extract title from path if we don't have it std::string title = bookPath; const size_t lastSlash = title.find_last_of('/'); if (lastSlash != std::string::npos) { title = title.substr(lastSlash + 1); } const size_t dot = title.find_last_of('.'); if (dot != std::string::npos) { title.resize(dot); } serialization::writeString(outputFile, bookPath); serialization::writeString(outputFile, title); serialization::writeString(outputFile, ""); // Author (not always available) outputFile.close(); Serial.printf("[%lu] [BMS] Bookmarks saved for %s (%d entries)\n", millis(), bookPath.c_str(), count); return true; } bool BookmarkStore::loadBookmarks(const std::string& bookPath, std::vector& bookmarks) { bookmarks.clear(); const std::string filePath = getBookmarksFilePath(bookPath); if (filePath.empty()) return false; FsFile inputFile; if (!SdMan.openFileForRead("BMS", filePath, inputFile)) { return false; } uint8_t version; serialization::readPod(inputFile, version); if (version != BOOKMARKS_FILE_VERSION) { Serial.printf("[%lu] [BMS] Unknown bookmarks file version: %u\n", millis(), version); inputFile.close(); return false; } uint8_t count; serialization::readPod(inputFile, count); bookmarks.reserve(count); for (uint8_t i = 0; i < count; i++) { Bookmark bookmark; serialization::readString(inputFile, bookmark.name); serialization::readPod(inputFile, bookmark.spineIndex); serialization::readPod(inputFile, bookmark.contentOffset); serialization::readPod(inputFile, bookmark.pageNumber); serialization::readPod(inputFile, bookmark.timestamp); bookmarks.push_back(bookmark); } inputFile.close(); Serial.printf("[%lu] [BMS] Bookmarks loaded for %s (%d entries)\n", millis(), bookPath.c_str(), count); return true; }