From 77bb97339d1b72d440d59690ffee2aa1ac57544b Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Sun, 4 Jan 2026 23:48:42 -0500 Subject: [PATCH] Add a setting for document matching method --- lib/KOReaderSync/KOReaderCredentialStore.cpp | 18 ++++++++ lib/KOReaderSync/KOReaderCredentialStore.h | 14 +++++- lib/KOReaderSync/KOReaderDocumentId.cpp | 27 +++++++++++ lib/KOReaderSync/KOReaderDocumentId.h | 11 ++++- .../EpubReaderChapterSelectionActivity.cpp | 45 +++++++++++-------- .../EpubReaderChapterSelectionActivity.h | 11 ++++- .../reader/KOReaderSyncActivity.cpp | 19 ++++++-- .../settings/KOReaderSettingsActivity.cpp | 14 +++++- 8 files changed, 133 insertions(+), 26 deletions(-) diff --git a/lib/KOReaderSync/KOReaderCredentialStore.cpp b/lib/KOReaderSync/KOReaderCredentialStore.cpp index 6eb37ac8..f73cc325 100644 --- a/lib/KOReaderSync/KOReaderCredentialStore.cpp +++ b/lib/KOReaderSync/KOReaderCredentialStore.cpp @@ -54,6 +54,9 @@ bool KOReaderCredentialStore::saveToFile() const { // Write server URL serialization::writeString(file, serverUrl); + // Write match method + serialization::writePod(file, static_cast(matchMethod)); + file.close(); Serial.printf("[%lu] [KRS] Saved KOReader credentials to file\n", millis()); return true; @@ -97,6 +100,15 @@ bool KOReaderCredentialStore::loadFromFile() { serverUrl.clear(); } + // Read match method + if (file.available()) { + uint8_t method; + serialization::readPod(file, method); + matchMethod = static_cast(method); + } else { + matchMethod = DocumentMatchMethod::FILENAME; + } + file.close(); Serial.printf("[%lu] [KRS] Loaded KOReader credentials for user: %s\n", millis(), username.c_str()); return true; @@ -148,3 +160,9 @@ std::string KOReaderCredentialStore::getBaseUrl() const { return serverUrl; } + +void KOReaderCredentialStore::setMatchMethod(DocumentMatchMethod method) { + matchMethod = method; + Serial.printf("[%lu] [KRS] Set match method: %s\n", millis(), + method == DocumentMatchMethod::FILENAME ? "Filename" : "Binary"); +} diff --git a/lib/KOReaderSync/KOReaderCredentialStore.h b/lib/KOReaderSync/KOReaderCredentialStore.h index 488f23b0..d1709660 100644 --- a/lib/KOReaderSync/KOReaderCredentialStore.h +++ b/lib/KOReaderSync/KOReaderCredentialStore.h @@ -1,6 +1,13 @@ #pragma once +#include #include +// Document matching method for KOReader sync +enum class DocumentMatchMethod : uint8_t { + FILENAME = 0, // Match by filename (simpler, works across different file sources) + BINARY = 1, // Match by partial MD5 of file content (more accurate, but files must be identical) +}; + /** * Singleton class for storing KOReader sync credentials on the SD card. * Credentials are stored in /sd/.crosspoint/koreader.bin with basic @@ -11,7 +18,8 @@ class KOReaderCredentialStore { static KOReaderCredentialStore instance; std::string username; std::string password; - std::string serverUrl; // Custom sync server URL (empty = default) + std::string serverUrl; // Custom sync server URL (empty = default) + DocumentMatchMethod matchMethod = DocumentMatchMethod::FILENAME; // Default to filename for compatibility // Private constructor for singleton KOReaderCredentialStore() = default; @@ -51,6 +59,10 @@ class KOReaderCredentialStore { // Get base URL for API calls (with https:// normalization, falls back to default) std::string getBaseUrl() const; + + // Document matching method + void setMatchMethod(DocumentMatchMethod method); + DocumentMatchMethod getMatchMethod() const { return matchMethod; } }; // Helper macro to access credential store diff --git a/lib/KOReaderSync/KOReaderDocumentId.cpp b/lib/KOReaderSync/KOReaderDocumentId.cpp index f75d7c8f..2c52464c 100644 --- a/lib/KOReaderSync/KOReaderDocumentId.cpp +++ b/lib/KOReaderSync/KOReaderDocumentId.cpp @@ -4,6 +4,33 @@ #include #include +namespace { +// Extract filename from path (everything after last '/') +std::string getFilename(const std::string& path) { + const size_t pos = path.rfind('/'); + if (pos == std::string::npos) { + return path; + } + return path.substr(pos + 1); +} +} // namespace + +std::string KOReaderDocumentId::calculateFromFilename(const std::string& filePath) { + const std::string filename = getFilename(filePath); + if (filename.empty()) { + return ""; + } + + MD5Builder md5; + md5.begin(); + md5.add(filename.c_str()); + md5.calculate(); + + std::string result = md5.toString().c_str(); + Serial.printf("[%lu] [KODoc] Filename hash: %s (from '%s')\n", millis(), result.c_str(), filename.c_str()); + return result; +} + size_t KOReaderDocumentId::getOffset(int i) { // Offset = 1024 << (2*i) // For i = -1: 1024 >> 2 = 256 diff --git a/lib/KOReaderSync/KOReaderDocumentId.h b/lib/KOReaderSync/KOReaderDocumentId.h index 434b9b63..2b6189e2 100644 --- a/lib/KOReaderSync/KOReaderDocumentId.h +++ b/lib/KOReaderSync/KOReaderDocumentId.h @@ -17,13 +17,22 @@ class KOReaderDocumentId { public: /** - * Calculate the KOReader document hash for a file. + * Calculate the KOReader document hash for a file (binary/content-based). * * @param filePath Path to the file (typically an EPUB) * @return 32-character lowercase hex string, or empty string on failure */ static std::string calculate(const std::string& filePath); + /** + * Calculate document hash from filename only (filename-based sync mode). + * This is simpler and works when files have the same name across devices. + * + * @param filePath Path to the file (only the filename portion is used) + * @return 32-character lowercase hex MD5 of the filename + */ + static std::string calculateFromFilename(const std::string& filePath); + private: // Size of each chunk to read at each offset static constexpr size_t CHUNK_SIZE = 1024; diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index 49c3051f..d20c1645 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -10,15 +10,26 @@ namespace { // Time threshold for treating a long press as a page-up/page-down constexpr int SKIP_PAGE_MS = 700; - -// Sync option is shown at index 0 if credentials are configured -constexpr int SYNC_ITEM_INDEX = 0; } // namespace +bool EpubReaderChapterSelectionActivity::hasSyncOption() const { return KOREADER_STORE.hasCredentials(); } + int EpubReaderChapterSelectionActivity::getTotalItems() const { - // Add 1 for sync option if credentials are configured - const int syncOffset = KOREADER_STORE.hasCredentials() ? 1 : 0; - return epub->getTocItemsCount() + syncOffset; + // Add 2 for sync options (top and bottom) if credentials are configured + const int syncCount = hasSyncOption() ? 2 : 0; + return epub->getTocItemsCount() + syncCount; +} + +bool EpubReaderChapterSelectionActivity::isSyncItem(int index) const { + if (!hasSyncOption()) return false; + // First item and last item are sync options + return index == 0 || index == getTotalItems() - 1; +} + +int EpubReaderChapterSelectionActivity::tocIndexFromItemIndex(int itemIndex) const { + // Account for the sync option at the top + const int offset = hasSyncOption() ? 1 : 0; + return itemIndex - offset; } int EpubReaderChapterSelectionActivity::getPageItems() const { @@ -52,12 +63,12 @@ void EpubReaderChapterSelectionActivity::onEnter() { renderingMutex = xSemaphoreCreateMutex(); // Account for sync option offset when finding current TOC index - const int syncOffset = KOREADER_STORE.hasCredentials() ? 1 : 0; + const int syncOffset = hasSyncOption() ? 1 : 0; selectorIndex = epub->getTocIndexForSpineIndex(currentSpineIndex); if (selectorIndex == -1) { selectorIndex = 0; } - selectorIndex += syncOffset; // Offset for sync option + selectorIndex += syncOffset; // Offset for top sync option // Trigger first update updateRequired = true; @@ -114,17 +125,16 @@ void EpubReaderChapterSelectionActivity::loop() { const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; const int pageItems = getPageItems(); const int totalItems = getTotalItems(); - const int syncOffset = KOREADER_STORE.hasCredentials() ? 1 : 0; if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { - // Check if sync option is selected - if (syncOffset > 0 && selectorIndex == SYNC_ITEM_INDEX) { + // Check if sync option is selected (first or last item) + if (isSyncItem(selectorIndex)) { launchSyncActivity(); return; } - // Get TOC index (account for sync offset) - const int tocIndex = selectorIndex - syncOffset; + // Get TOC index (account for top sync offset) + const int tocIndex = tocIndexFromItemIndex(selectorIndex); const auto newSpineIndex = epub->getSpineIndexForTocIndex(tocIndex); if (newSpineIndex == -1) { onGoBack(); @@ -168,7 +178,6 @@ void EpubReaderChapterSelectionActivity::renderScreen() { const auto pageWidth = renderer.getScreenWidth(); const int pageItems = getPageItems(); const int totalItems = getTotalItems(); - const int syncOffset = KOREADER_STORE.hasCredentials() ? 1 : 0; const std::string title = renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(), pageWidth - 40, EpdFontFamily::BOLD); @@ -181,12 +190,12 @@ void EpubReaderChapterSelectionActivity::renderScreen() { const int displayY = 60 + (itemIndex % pageItems) * 30; const bool isSelected = (itemIndex == selectorIndex); - if (syncOffset > 0 && itemIndex == SYNC_ITEM_INDEX) { - // Draw sync option + if (isSyncItem(itemIndex)) { + // Draw sync option (at top or bottom) renderer.drawText(UI_10_FONT_ID, 20, displayY, ">> Sync Progress", !isSelected); } else { - // Draw TOC item (account for sync offset) - const int tocIndex = itemIndex - syncOffset; + // Draw TOC item (account for top sync offset) + const int tocIndex = tocIndexFromItemIndex(itemIndex); auto item = epub->getTocItem(tocIndex); renderer.drawText(UI_10_FONT_ID, 20 + (item.level - 1) * 15, displayY, item.title.c_str(), !isSelected); } diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.h b/src/activities/reader/EpubReaderChapterSelectionActivity.h index 4bfef6da..255f0cea 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.h +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.h @@ -26,9 +26,18 @@ class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity // This adapts automatically when switching between portrait and landscape. int getPageItems() const; - // Total items including the sync option + // Total items including sync options (top and bottom) int getTotalItems() const; + // Check if sync option is available (credentials configured) + bool hasSyncOption() const; + + // Check if given item index is a sync option (first or last) + bool isSyncItem(int index) const; + + // Convert item index to TOC index (accounting for top sync option offset) + int tocIndexFromItemIndex(int itemIndex) const; + static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); void renderScreen(); diff --git a/src/activities/reader/KOReaderSyncActivity.cpp b/src/activities/reader/KOReaderSyncActivity.cpp index 80bb3b27..59ae4eed 100644 --- a/src/activities/reader/KOReaderSyncActivity.cpp +++ b/src/activities/reader/KOReaderSyncActivity.cpp @@ -12,6 +12,11 @@ namespace { void syncTimeWithNTP() { + // Stop SNTP if already running (can't reconfigure while running) + if (esp_sntp_enabled()) { + esp_sntp_stop(); + } + // Configure SNTP esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL); esp_sntp_setservername(0, "pool.ntp.org"); @@ -67,8 +72,12 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { } void KOReaderSyncActivity::performSync() { - // Calculate document hash - documentHash = KOReaderDocumentId::calculate(epubPath); + // Calculate document hash based on user's preferred method + if (KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME) { + documentHash = KOReaderDocumentId::calculateFromFilename(epubPath); + } else { + documentHash = KOReaderDocumentId::calculate(epubPath); + } if (documentHash.empty()) { xSemaphoreTake(renderingMutex, portMAX_DELAY); state = SYNC_FAILED; @@ -413,7 +422,11 @@ void KOReaderSyncActivity::loop() { if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { // Calculate hash if not done yet if (documentHash.empty()) { - documentHash = KOReaderDocumentId::calculate(epubPath); + if (KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME) { + documentHash = KOReaderDocumentId::calculateFromFilename(epubPath); + } else { + documentHash = KOReaderDocumentId::calculate(epubPath); + } } performUpload(); } diff --git a/src/activities/settings/KOReaderSettingsActivity.cpp b/src/activities/settings/KOReaderSettingsActivity.cpp index 4b16e966..b186e51a 100644 --- a/src/activities/settings/KOReaderSettingsActivity.cpp +++ b/src/activities/settings/KOReaderSettingsActivity.cpp @@ -11,8 +11,8 @@ #include "fontIds.h" namespace { -constexpr int MENU_ITEMS = 4; -const char* menuNames[MENU_ITEMS] = {"Username", "Password", "Sync Server URL", "Authenticate"}; +constexpr int MENU_ITEMS = 5; +const char* menuNames[MENU_ITEMS] = {"Username", "Password", "Sync Server URL", "Document Matching", "Authenticate"}; } // namespace void KOReaderSettingsActivity::taskTrampoline(void* param) { @@ -129,6 +129,14 @@ void KOReaderSettingsActivity::handleSelection() { updateRequired = true; })); } else if (selectedIndex == 3) { + // Document Matching - toggle between Filename and Binary + const auto current = KOREADER_STORE.getMatchMethod(); + const auto newMethod = (current == DocumentMatchMethod::FILENAME) ? DocumentMatchMethod::BINARY + : DocumentMatchMethod::FILENAME; + KOREADER_STORE.setMatchMethod(newMethod); + KOREADER_STORE.saveToFile(); + updateRequired = true; + } else if (selectedIndex == 4) { // Authenticate if (!KOREADER_STORE.hasCredentials()) { // Can't authenticate without credentials - just show message briefly @@ -184,6 +192,8 @@ void KOReaderSettingsActivity::render() { } else if (i == 2) { status = KOREADER_STORE.getServerUrl().empty() ? "[Not Set]" : "[Set]"; } else if (i == 3) { + status = KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? "[Filename]" : "[Binary]"; + } else if (i == 4) { status = KOREADER_STORE.hasCredentials() ? "" : "[Set credentials first]"; }