#pragma once #include #include #include #include #include #include #include "Bookmark.h" // Stores starred/bookmarked pages for a single book. // Persisted as a binary file on SD card within the book's cache directory. class BookmarkStore { public: using Bookmark = ::Bookmark; // Load bookmarks from the cache directory (e.g. .crosspoint/epub_/). void load(const std::string& cachePath) { basePath = cachePath; bookmarks.clear(); dirty = false; FsFile f; if (!Storage.openFileForRead("BKM", getFilePath(), f)) { return; } uint8_t version; if (f.read(reinterpret_cast(&version), sizeof(version)) != sizeof(version) || version < 1 || version > FILE_VERSION) { f.close(); return; } uint16_t count; if (f.read(reinterpret_cast(&count), sizeof(count)) != sizeof(count) || count > MAX_BOOKMARKS) { LOG_ERR("BKM", "Invalid bookmark count: %u", static_cast(count)); f.close(); return; } bookmarks.reserve(count); for (uint16_t i = 0; i < count; i++) { Bookmark bm; if (f.read(reinterpret_cast(&bm.spineIndex), sizeof(bm.spineIndex)) != sizeof(bm.spineIndex) || f.read(reinterpret_cast(&bm.pageNumber), sizeof(bm.pageNumber)) != sizeof(bm.pageNumber)) { LOG_ERR("BKM", "Truncated bookmarks file at entry %d", i); bookmarks.clear(); f.close(); return; } if (version >= 2) { uint16_t nameLen = 0; if (f.read(reinterpret_cast(&nameLen), sizeof(nameLen)) != sizeof(nameLen) || nameLen > MAX_NAME_LENGTH) { LOG_ERR("BKM", "Invalid bookmark name length at entry %d", i); bookmarks.clear(); f.close(); return; } if (nameLen > 0) { bm.name.resize(nameLen); if (f.read(reinterpret_cast(&bm.name[0]), nameLen) != nameLen) { LOG_ERR("BKM", "Truncated bookmark name at entry %d", i); bookmarks.clear(); f.close(); return; } } } bookmarks.push_back(std::move(bm)); } f.close(); LOG_DBG("BKM", "Loaded %d bookmarks", count); } // Save bookmarks to SD card (only if changed). void save() { if (!dirty || basePath.empty()) { return; } if (bookmarks.size() > MAX_BOOKMARKS) { LOG_ERR("BKM", "Too many bookmarks to save: %u", static_cast(bookmarks.size())); return; } FsFile f; if (!Storage.openFileForWrite("BKM", getFilePath(), f)) { LOG_ERR("BKM", "Failed to save bookmarks"); return; } auto writePodChecked = [&f](const auto& value) { return f.write(reinterpret_cast(&value), sizeof(value)) == sizeof(value); }; const uint16_t count = static_cast(bookmarks.size()); bool ok = writePodChecked(FILE_VERSION) && writePodChecked(count); for (const auto& bm : bookmarks) { const uint16_t nameLen = static_cast(std::min(bm.name.size(), MAX_NAME_LENGTH)); ok = ok && writePodChecked(bm.spineIndex) && writePodChecked(bm.pageNumber) && writePodChecked(nameLen); if (ok && nameLen > 0) { ok = f.write(reinterpret_cast(bm.name.data()), nameLen) == nameLen; } } if (ok) { if (!f.close()) { LOG_ERR("BKM", "Failed to close bookmarks file"); return; } } else { f.close(); LOG_ERR("BKM", "Failed while writing bookmarks"); return; } dirty = false; LOG_DBG("BKM", "Saved %d bookmarks", count); } // Toggle bookmark for the given page. Returns true if now starred, false if removed. bool toggle(uint16_t spineIndex, uint16_t pageNumber) { auto it = find(spineIndex, pageNumber); if (it != bookmarks.end()) { bookmarks.erase(it); dirty = true; return false; } bookmarks.push_back({spineIndex, pageNumber}); dirty = true; return true; } // Check if a page is starred. [[nodiscard]] bool has(uint16_t spineIndex, uint16_t pageNumber) const { return std::any_of(bookmarks.begin(), bookmarks.end(), [spineIndex, pageNumber](const Bookmark& bm) { return bm.spineIndex == spineIndex && bm.pageNumber == pageNumber; }); } [[nodiscard]] const std::vector& getAll() const { return bookmarks; } [[nodiscard]] bool isEmpty() const { return bookmarks.empty(); } void markDirty() { dirty = true; } // Set or clear the name for the bookmark at index. Empty name reverts to default label. void rename(size_t index, std::string name) { if (index >= bookmarks.size()) return; if (name.size() > MAX_NAME_LENGTH) name.resize(MAX_NAME_LENGTH); bookmarks[index].name = std::move(name); dirty = true; } void removeAt(size_t index) { if (index >= bookmarks.size()) return; bookmarks.erase(bookmarks.begin() + index); dirty = true; } static constexpr uint16_t MAX_NAME_LENGTH = 128; private: static constexpr uint8_t FILE_VERSION = 2; static constexpr uint16_t MAX_BOOKMARKS = 1000; std::vector bookmarks; std::string basePath; bool dirty = false; [[nodiscard]] std::string getFilePath() const { return basePath + "/bookmarks.bin"; } std::vector::iterator find(uint16_t spineIndex, uint16_t pageNumber) { return std::find_if(bookmarks.begin(), bookmarks.end(), [spineIndex, pageNumber](const Bookmark& bm) { return bm.spineIndex == spineIndex && bm.pageNumber == pageNumber; }); } };