2026-02-12 20:40:07 -05:00
|
|
|
#include "BookmarkStore.h"
|
|
|
|
|
|
|
|
|
|
#include <HalStorage.h>
|
2026-02-13 16:27:58 -05:00
|
|
|
#include <Logging.h>
|
2026-02-12 20:40:07 -05:00
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
|
|
|
|
|
std::string BookmarkStore::filePath(const std::string& cachePath) { return cachePath + "/bookmarks.bin"; }
|
|
|
|
|
|
|
|
|
|
std::vector<Bookmark> BookmarkStore::load(const std::string& cachePath) {
|
|
|
|
|
std::vector<Bookmark> bookmarks;
|
|
|
|
|
FsFile f;
|
|
|
|
|
if (!Storage.openFileForRead("BKM", filePath(cachePath), f)) {
|
|
|
|
|
return bookmarks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// File format v2: [version(1)] [count(2)] [entries...]
|
|
|
|
|
// Each entry: [spine(2)] [page(2)] [snippetLen(1)] [snippet(snippetLen)]
|
|
|
|
|
// v1 (no version byte): [count(2)] [entries of 4 bytes each]
|
|
|
|
|
// We detect v1 by checking if the first byte could be a version marker (0xFF).
|
|
|
|
|
|
|
|
|
|
uint8_t firstByte;
|
|
|
|
|
if (f.read(&firstByte, 1) != 1) {
|
|
|
|
|
f.close();
|
|
|
|
|
return bookmarks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint16_t count;
|
|
|
|
|
bool hasSnippets;
|
|
|
|
|
|
|
|
|
|
if (firstByte == 0xFF) {
|
|
|
|
|
// v2 format: version marker was 0xFF
|
|
|
|
|
hasSnippets = true;
|
|
|
|
|
uint8_t countBytes[2];
|
|
|
|
|
if (f.read(countBytes, 2) != 2) {
|
|
|
|
|
f.close();
|
|
|
|
|
return bookmarks;
|
|
|
|
|
}
|
|
|
|
|
count = static_cast<uint16_t>(countBytes[0]) | (static_cast<uint16_t>(countBytes[1]) << 8);
|
|
|
|
|
} else {
|
|
|
|
|
// v1 format: first byte was part of the count
|
|
|
|
|
hasSnippets = false;
|
|
|
|
|
uint8_t secondByte;
|
|
|
|
|
if (f.read(&secondByte, 1) != 1) {
|
|
|
|
|
f.close();
|
|
|
|
|
return bookmarks;
|
|
|
|
|
}
|
|
|
|
|
count = static_cast<uint16_t>(firstByte) | (static_cast<uint16_t>(secondByte) << 8);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (count > MAX_BOOKMARKS) {
|
|
|
|
|
count = MAX_BOOKMARKS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (uint16_t i = 0; i < count; i++) {
|
|
|
|
|
uint8_t entry[4];
|
|
|
|
|
if (f.read(entry, 4) != 4) break;
|
|
|
|
|
Bookmark b;
|
|
|
|
|
b.spineIndex = static_cast<int16_t>(static_cast<uint16_t>(entry[0]) | (static_cast<uint16_t>(entry[1]) << 8));
|
|
|
|
|
b.pageNumber = static_cast<int16_t>(static_cast<uint16_t>(entry[2]) | (static_cast<uint16_t>(entry[3]) << 8));
|
|
|
|
|
|
|
|
|
|
if (hasSnippets) {
|
|
|
|
|
uint8_t snippetLen;
|
|
|
|
|
if (f.read(&snippetLen, 1) != 1) break;
|
|
|
|
|
if (snippetLen > 0) {
|
|
|
|
|
std::vector<uint8_t> buf(snippetLen);
|
|
|
|
|
if (f.read(buf.data(), snippetLen) != snippetLen) break;
|
|
|
|
|
b.snippet = std::string(buf.begin(), buf.end());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bookmarks.push_back(b);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f.close();
|
|
|
|
|
return bookmarks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool BookmarkStore::save(const std::string& cachePath, const std::vector<Bookmark>& bookmarks) {
|
|
|
|
|
FsFile f;
|
|
|
|
|
if (!Storage.openFileForWrite("BKM", filePath(cachePath), f)) {
|
2026-02-13 16:27:58 -05:00
|
|
|
LOG_ERR("BKM", "Could not save bookmarks!");
|
2026-02-12 20:40:07 -05:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write v2 format: version marker + count + entries with snippets
|
|
|
|
|
uint8_t version = 0xFF;
|
|
|
|
|
f.write(&version, 1);
|
|
|
|
|
|
|
|
|
|
uint16_t count = static_cast<uint16_t>(bookmarks.size());
|
|
|
|
|
uint8_t header[2] = {static_cast<uint8_t>(count & 0xFF), static_cast<uint8_t>((count >> 8) & 0xFF)};
|
|
|
|
|
f.write(header, 2);
|
|
|
|
|
|
|
|
|
|
for (const auto& b : bookmarks) {
|
|
|
|
|
uint8_t entry[4];
|
|
|
|
|
entry[0] = static_cast<uint8_t>(b.spineIndex & 0xFF);
|
|
|
|
|
entry[1] = static_cast<uint8_t>((b.spineIndex >> 8) & 0xFF);
|
|
|
|
|
entry[2] = static_cast<uint8_t>(b.pageNumber & 0xFF);
|
|
|
|
|
entry[3] = static_cast<uint8_t>((b.pageNumber >> 8) & 0xFF);
|
|
|
|
|
f.write(entry, 4);
|
|
|
|
|
|
|
|
|
|
// Write snippet: length byte + string data
|
|
|
|
|
uint8_t snippetLen = static_cast<uint8_t>(std::min(static_cast<int>(b.snippet.size()), MAX_SNIPPET_LENGTH));
|
|
|
|
|
f.write(&snippetLen, 1);
|
|
|
|
|
if (snippetLen > 0) {
|
|
|
|
|
f.write(reinterpret_cast<const uint8_t*>(b.snippet.c_str()), snippetLen);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f.close();
|
2026-02-13 16:27:58 -05:00
|
|
|
LOG_DBG("BKM", "Saved %d bookmarks", count);
|
2026-02-12 20:40:07 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool BookmarkStore::addBookmark(const std::string& cachePath, int spineIndex, int page, const std::string& snippet) {
|
|
|
|
|
auto bookmarks = load(cachePath);
|
|
|
|
|
|
|
|
|
|
// Check for duplicate
|
|
|
|
|
for (const auto& b : bookmarks) {
|
|
|
|
|
if (b.spineIndex == spineIndex && b.pageNumber == page) {
|
|
|
|
|
return true; // Already bookmarked
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (static_cast<int>(bookmarks.size()) >= MAX_BOOKMARKS) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Bookmark b;
|
|
|
|
|
b.spineIndex = static_cast<int16_t>(spineIndex);
|
|
|
|
|
b.pageNumber = static_cast<int16_t>(page);
|
|
|
|
|
b.snippet = snippet.substr(0, MAX_SNIPPET_LENGTH);
|
|
|
|
|
bookmarks.push_back(b);
|
|
|
|
|
|
|
|
|
|
return save(cachePath, bookmarks);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool BookmarkStore::removeBookmark(const std::string& cachePath, int spineIndex, int page) {
|
|
|
|
|
auto bookmarks = load(cachePath);
|
|
|
|
|
|
|
|
|
|
auto it = std::remove_if(bookmarks.begin(), bookmarks.end(),
|
|
|
|
|
[spineIndex, page](const Bookmark& b) {
|
|
|
|
|
return b.spineIndex == spineIndex && b.pageNumber == page;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (it == bookmarks.end()) {
|
|
|
|
|
return false; // Not found
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bookmarks.erase(it, bookmarks.end());
|
|
|
|
|
return save(cachePath, bookmarks);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool BookmarkStore::hasBookmark(const std::string& cachePath, int spineIndex, int page) {
|
|
|
|
|
auto bookmarks = load(cachePath);
|
|
|
|
|
return std::any_of(bookmarks.begin(), bookmarks.end(), [spineIndex, page](const Bookmark& b) {
|
|
|
|
|
return b.spineIndex == spineIndex && b.pageNumber == page;
|
|
|
|
|
});
|
|
|
|
|
}
|