crosspoint-reader/src/BookmarkStore.cpp

302 lines
9.8 KiB
C++
Raw Normal View History

#include "BookmarkStore.h"
#include <HardwareSerial.h>
#include <SDCardManager.h>
#include <Serialization.h>
#include <algorithm>
#include <functional>
#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<std::string>{}(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<Bookmark> BookmarkStore::getBookmarks(const std::string& bookPath) {
std::vector<Bookmark> bookmarks;
loadBookmarks(bookPath, bookmarks);
return bookmarks;
}
bool BookmarkStore::addBookmark(const std::string& bookPath, const Bookmark& bookmark) {
std::vector<Bookmark> 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<Bookmark> 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<Bookmark> 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<BookmarkedBook> BookmarkStore::getBooksWithBookmarks() {
std::vector<BookmarkedBook> 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<Bookmark>& 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<uint8_t>(std::min(bookmarks.size(), static_cast<size_t>(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<Bookmark>& 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;
}