mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-05 23:27:38 +03:00
Add a setting for document matching method
This commit is contained in:
parent
ae34fc76e1
commit
77bb97339d
@ -54,6 +54,9 @@ bool KOReaderCredentialStore::saveToFile() const {
|
|||||||
// Write server URL
|
// Write server URL
|
||||||
serialization::writeString(file, serverUrl);
|
serialization::writeString(file, serverUrl);
|
||||||
|
|
||||||
|
// Write match method
|
||||||
|
serialization::writePod(file, static_cast<uint8_t>(matchMethod));
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
Serial.printf("[%lu] [KRS] Saved KOReader credentials to file\n", millis());
|
Serial.printf("[%lu] [KRS] Saved KOReader credentials to file\n", millis());
|
||||||
return true;
|
return true;
|
||||||
@ -97,6 +100,15 @@ bool KOReaderCredentialStore::loadFromFile() {
|
|||||||
serverUrl.clear();
|
serverUrl.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read match method
|
||||||
|
if (file.available()) {
|
||||||
|
uint8_t method;
|
||||||
|
serialization::readPod(file, method);
|
||||||
|
matchMethod = static_cast<DocumentMatchMethod>(method);
|
||||||
|
} else {
|
||||||
|
matchMethod = DocumentMatchMethod::FILENAME;
|
||||||
|
}
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
Serial.printf("[%lu] [KRS] Loaded KOReader credentials for user: %s\n", millis(), username.c_str());
|
Serial.printf("[%lu] [KRS] Loaded KOReader credentials for user: %s\n", millis(), username.c_str());
|
||||||
return true;
|
return true;
|
||||||
@ -148,3 +160,9 @@ std::string KOReaderCredentialStore::getBaseUrl() const {
|
|||||||
|
|
||||||
return serverUrl;
|
return serverUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void KOReaderCredentialStore::setMatchMethod(DocumentMatchMethod method) {
|
||||||
|
matchMethod = method;
|
||||||
|
Serial.printf("[%lu] [KRS] Set match method: %s\n", millis(),
|
||||||
|
method == DocumentMatchMethod::FILENAME ? "Filename" : "Binary");
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
// 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.
|
* Singleton class for storing KOReader sync credentials on the SD card.
|
||||||
* Credentials are stored in /sd/.crosspoint/koreader.bin with basic
|
* Credentials are stored in /sd/.crosspoint/koreader.bin with basic
|
||||||
@ -11,7 +18,8 @@ class KOReaderCredentialStore {
|
|||||||
static KOReaderCredentialStore instance;
|
static KOReaderCredentialStore instance;
|
||||||
std::string username;
|
std::string username;
|
||||||
std::string password;
|
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
|
// Private constructor for singleton
|
||||||
KOReaderCredentialStore() = default;
|
KOReaderCredentialStore() = default;
|
||||||
@ -51,6 +59,10 @@ class KOReaderCredentialStore {
|
|||||||
|
|
||||||
// Get base URL for API calls (with https:// normalization, falls back to default)
|
// Get base URL for API calls (with https:// normalization, falls back to default)
|
||||||
std::string getBaseUrl() const;
|
std::string getBaseUrl() const;
|
||||||
|
|
||||||
|
// Document matching method
|
||||||
|
void setMatchMethod(DocumentMatchMethod method);
|
||||||
|
DocumentMatchMethod getMatchMethod() const { return matchMethod; }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper macro to access credential store
|
// Helper macro to access credential store
|
||||||
|
|||||||
@ -4,6 +4,33 @@
|
|||||||
#include <MD5Builder.h>
|
#include <MD5Builder.h>
|
||||||
#include <SDCardManager.h>
|
#include <SDCardManager.h>
|
||||||
|
|
||||||
|
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) {
|
size_t KOReaderDocumentId::getOffset(int i) {
|
||||||
// Offset = 1024 << (2*i)
|
// Offset = 1024 << (2*i)
|
||||||
// For i = -1: 1024 >> 2 = 256
|
// For i = -1: 1024 >> 2 = 256
|
||||||
|
|||||||
@ -17,13 +17,22 @@
|
|||||||
class KOReaderDocumentId {
|
class KOReaderDocumentId {
|
||||||
public:
|
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)
|
* @param filePath Path to the file (typically an EPUB)
|
||||||
* @return 32-character lowercase hex string, or empty string on failure
|
* @return 32-character lowercase hex string, or empty string on failure
|
||||||
*/
|
*/
|
||||||
static std::string calculate(const std::string& filePath);
|
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:
|
private:
|
||||||
// Size of each chunk to read at each offset
|
// Size of each chunk to read at each offset
|
||||||
static constexpr size_t CHUNK_SIZE = 1024;
|
static constexpr size_t CHUNK_SIZE = 1024;
|
||||||
|
|||||||
@ -10,15 +10,26 @@
|
|||||||
namespace {
|
namespace {
|
||||||
// Time threshold for treating a long press as a page-up/page-down
|
// Time threshold for treating a long press as a page-up/page-down
|
||||||
constexpr int SKIP_PAGE_MS = 700;
|
constexpr int SKIP_PAGE_MS = 700;
|
||||||
|
|
||||||
// Sync option is shown at index 0 if credentials are configured
|
|
||||||
constexpr int SYNC_ITEM_INDEX = 0;
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
bool EpubReaderChapterSelectionActivity::hasSyncOption() const { return KOREADER_STORE.hasCredentials(); }
|
||||||
|
|
||||||
int EpubReaderChapterSelectionActivity::getTotalItems() const {
|
int EpubReaderChapterSelectionActivity::getTotalItems() const {
|
||||||
// Add 1 for sync option if credentials are configured
|
// Add 2 for sync options (top and bottom) if credentials are configured
|
||||||
const int syncOffset = KOREADER_STORE.hasCredentials() ? 1 : 0;
|
const int syncCount = hasSyncOption() ? 2 : 0;
|
||||||
return epub->getTocItemsCount() + syncOffset;
|
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 {
|
int EpubReaderChapterSelectionActivity::getPageItems() const {
|
||||||
@ -52,12 +63,12 @@ void EpubReaderChapterSelectionActivity::onEnter() {
|
|||||||
renderingMutex = xSemaphoreCreateMutex();
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
// Account for sync option offset when finding current TOC index
|
// 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);
|
selectorIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
|
||||||
if (selectorIndex == -1) {
|
if (selectorIndex == -1) {
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
}
|
}
|
||||||
selectorIndex += syncOffset; // Offset for sync option
|
selectorIndex += syncOffset; // Offset for top sync option
|
||||||
|
|
||||||
// Trigger first update
|
// Trigger first update
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
@ -114,17 +125,16 @@ void EpubReaderChapterSelectionActivity::loop() {
|
|||||||
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS;
|
||||||
const int pageItems = getPageItems();
|
const int pageItems = getPageItems();
|
||||||
const int totalItems = getTotalItems();
|
const int totalItems = getTotalItems();
|
||||||
const int syncOffset = KOREADER_STORE.hasCredentials() ? 1 : 0;
|
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
// Check if sync option is selected
|
// Check if sync option is selected (first or last item)
|
||||||
if (syncOffset > 0 && selectorIndex == SYNC_ITEM_INDEX) {
|
if (isSyncItem(selectorIndex)) {
|
||||||
launchSyncActivity();
|
launchSyncActivity();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get TOC index (account for sync offset)
|
// Get TOC index (account for top sync offset)
|
||||||
const int tocIndex = selectorIndex - syncOffset;
|
const int tocIndex = tocIndexFromItemIndex(selectorIndex);
|
||||||
const auto newSpineIndex = epub->getSpineIndexForTocIndex(tocIndex);
|
const auto newSpineIndex = epub->getSpineIndexForTocIndex(tocIndex);
|
||||||
if (newSpineIndex == -1) {
|
if (newSpineIndex == -1) {
|
||||||
onGoBack();
|
onGoBack();
|
||||||
@ -168,7 +178,6 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
|
|||||||
const auto pageWidth = renderer.getScreenWidth();
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
const int pageItems = getPageItems();
|
const int pageItems = getPageItems();
|
||||||
const int totalItems = getTotalItems();
|
const int totalItems = getTotalItems();
|
||||||
const int syncOffset = KOREADER_STORE.hasCredentials() ? 1 : 0;
|
|
||||||
|
|
||||||
const std::string title =
|
const std::string title =
|
||||||
renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(), pageWidth - 40, EpdFontFamily::BOLD);
|
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 int displayY = 60 + (itemIndex % pageItems) * 30;
|
||||||
const bool isSelected = (itemIndex == selectorIndex);
|
const bool isSelected = (itemIndex == selectorIndex);
|
||||||
|
|
||||||
if (syncOffset > 0 && itemIndex == SYNC_ITEM_INDEX) {
|
if (isSyncItem(itemIndex)) {
|
||||||
// Draw sync option
|
// Draw sync option (at top or bottom)
|
||||||
renderer.drawText(UI_10_FONT_ID, 20, displayY, ">> Sync Progress", !isSelected);
|
renderer.drawText(UI_10_FONT_ID, 20, displayY, ">> Sync Progress", !isSelected);
|
||||||
} else {
|
} else {
|
||||||
// Draw TOC item (account for sync offset)
|
// Draw TOC item (account for top sync offset)
|
||||||
const int tocIndex = itemIndex - syncOffset;
|
const int tocIndex = tocIndexFromItemIndex(itemIndex);
|
||||||
auto item = epub->getTocItem(tocIndex);
|
auto item = epub->getTocItem(tocIndex);
|
||||||
renderer.drawText(UI_10_FONT_ID, 20 + (item.level - 1) * 15, displayY, item.title.c_str(), !isSelected);
|
renderer.drawText(UI_10_FONT_ID, 20 + (item.level - 1) * 15, displayY, item.title.c_str(), !isSelected);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,9 +26,18 @@ class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity
|
|||||||
// This adapts automatically when switching between portrait and landscape.
|
// This adapts automatically when switching between portrait and landscape.
|
||||||
int getPageItems() const;
|
int getPageItems() const;
|
||||||
|
|
||||||
// Total items including the sync option
|
// Total items including sync options (top and bottom)
|
||||||
int getTotalItems() const;
|
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);
|
static void taskTrampoline(void* param);
|
||||||
[[noreturn]] void displayTaskLoop();
|
[[noreturn]] void displayTaskLoop();
|
||||||
void renderScreen();
|
void renderScreen();
|
||||||
|
|||||||
@ -12,6 +12,11 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
void syncTimeWithNTP() {
|
void syncTimeWithNTP() {
|
||||||
|
// Stop SNTP if already running (can't reconfigure while running)
|
||||||
|
if (esp_sntp_enabled()) {
|
||||||
|
esp_sntp_stop();
|
||||||
|
}
|
||||||
|
|
||||||
// Configure SNTP
|
// Configure SNTP
|
||||||
esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
|
esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
|
||||||
esp_sntp_setservername(0, "pool.ntp.org");
|
esp_sntp_setservername(0, "pool.ntp.org");
|
||||||
@ -67,8 +72,12 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void KOReaderSyncActivity::performSync() {
|
void KOReaderSyncActivity::performSync() {
|
||||||
// Calculate document hash
|
// Calculate document hash based on user's preferred method
|
||||||
documentHash = KOReaderDocumentId::calculate(epubPath);
|
if (KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME) {
|
||||||
|
documentHash = KOReaderDocumentId::calculateFromFilename(epubPath);
|
||||||
|
} else {
|
||||||
|
documentHash = KOReaderDocumentId::calculate(epubPath);
|
||||||
|
}
|
||||||
if (documentHash.empty()) {
|
if (documentHash.empty()) {
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
state = SYNC_FAILED;
|
state = SYNC_FAILED;
|
||||||
@ -413,7 +422,11 @@ void KOReaderSyncActivity::loop() {
|
|||||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
// Calculate hash if not done yet
|
// Calculate hash if not done yet
|
||||||
if (documentHash.empty()) {
|
if (documentHash.empty()) {
|
||||||
documentHash = KOReaderDocumentId::calculate(epubPath);
|
if (KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME) {
|
||||||
|
documentHash = KOReaderDocumentId::calculateFromFilename(epubPath);
|
||||||
|
} else {
|
||||||
|
documentHash = KOReaderDocumentId::calculate(epubPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
performUpload();
|
performUpload();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,8 +11,8 @@
|
|||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int MENU_ITEMS = 4;
|
constexpr int MENU_ITEMS = 5;
|
||||||
const char* menuNames[MENU_ITEMS] = {"Username", "Password", "Sync Server URL", "Authenticate"};
|
const char* menuNames[MENU_ITEMS] = {"Username", "Password", "Sync Server URL", "Document Matching", "Authenticate"};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void KOReaderSettingsActivity::taskTrampoline(void* param) {
|
void KOReaderSettingsActivity::taskTrampoline(void* param) {
|
||||||
@ -129,6 +129,14 @@ void KOReaderSettingsActivity::handleSelection() {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}));
|
}));
|
||||||
} else if (selectedIndex == 3) {
|
} 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
|
// Authenticate
|
||||||
if (!KOREADER_STORE.hasCredentials()) {
|
if (!KOREADER_STORE.hasCredentials()) {
|
||||||
// Can't authenticate without credentials - just show message briefly
|
// Can't authenticate without credentials - just show message briefly
|
||||||
@ -184,6 +192,8 @@ void KOReaderSettingsActivity::render() {
|
|||||||
} else if (i == 2) {
|
} else if (i == 2) {
|
||||||
status = KOREADER_STORE.getServerUrl().empty() ? "[Not Set]" : "[Set]";
|
status = KOREADER_STORE.getServerUrl().empty() ? "[Not Set]" : "[Set]";
|
||||||
} else if (i == 3) {
|
} else if (i == 3) {
|
||||||
|
status = KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? "[Filename]" : "[Binary]";
|
||||||
|
} else if (i == 4) {
|
||||||
status = KOREADER_STORE.hasCredentials() ? "" : "[Set credentials first]";
|
status = KOREADER_STORE.hasCredentials() ? "" : "[Set credentials first]";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user