From ad6036c162b3093f7c2c964e22f4a414c66f87d0 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Mon, 23 Mar 2026 12:23:21 +0100 Subject: [PATCH 1/3] regular menu action if no koreader credentials are defined --- src/activities/reader/EpubReaderActivity.cpp | 37 ++++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 3b8f16a3..d5c3a786 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -135,27 +135,26 @@ void EpubReaderActivity::loop() { } } - // Long press CONFIRM (1s+) goes directly to KOReader sync. + // Long press CONFIRM (1s+) goes directly to KOReader sync when credentials are configured. + // Without credentials, fall through to the regular menu on release. if (mappedInput.isPressed(MappedInputManager::Button::Confirm) && - mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) { - if (KOREADER_STORE.hasCredentials()) { - const int currentPage = section ? section->currentPage : 0; - const int totalPages = section ? section->pageCount : 0; - startActivityForResult( - std::make_unique(renderer, mappedInput, epub, epub->getPath(), currentSpineIndex, - currentPage, totalPages), - [this](const ActivityResult& result) { - if (!result.isCancelled) { - const auto& sync = std::get(result.data); - if (currentSpineIndex != sync.spineIndex || (section && section->currentPage != sync.page)) { - RenderLock lock(*this); - currentSpineIndex = sync.spineIndex; - nextPageNumber = sync.page; - section.reset(); - } + mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS && KOREADER_STORE.hasCredentials()) { + const int currentPage = section ? section->currentPage : 0; + const int totalPages = section ? section->pageCount : 0; + startActivityForResult( + std::make_unique(renderer, mappedInput, epub, epub->getPath(), currentSpineIndex, + currentPage, totalPages), + [this](const ActivityResult& result) { + if (!result.isCancelled) { + const auto& sync = std::get(result.data); + if (currentSpineIndex != sync.spineIndex || (section && section->currentPage != sync.page)) { + RenderLock lock(*this); + currentSpineIndex = sync.spineIndex; + nextPageNumber = sync.page; + section.reset(); } - }); - } + } + }); return; } From 68bf80e6b97fa5e1b69809036fb383d4005a36c5 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Mon, 23 Mar 2026 12:26:18 +0100 Subject: [PATCH 2/3] Suppress sync menu entry if no credentials are defined --- src/activities/reader/EpubReaderMenuActivity.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 1d95d9b7..abc58e2e 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -3,6 +3,7 @@ #include #include +#include "KOReaderCredentialStore.h" #include "MappedInputManager.h" #include "components/UITheme.h" #include "fontIds.h" @@ -32,7 +33,9 @@ std::vector EpubReaderMenuActivity::buildMenuI items.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON}); items.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR}); items.push_back({MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON}); - items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS}); + if (KOREADER_STORE.hasCredentials()) { + items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS}); + } items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}); return items; } From 75350b0a3eaf27f6b79b04e0d774ff88d216e249 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Mon, 23 Mar 2026 12:35:01 +0100 Subject: [PATCH 3/3] Incorporate koreader registration --- lib/I18n/translations/english.yaml | 9 ++- lib/KOReaderSync/KOReaderSyncClient.cpp | 65 +++++++++++++++++++ lib/KOReaderSync/KOReaderSyncClient.h | 26 ++++++-- .../settings/KOReaderAuthActivity.cpp | 47 ++++++++++++-- .../settings/KOReaderAuthActivity.h | 16 +++-- .../settings/KOReaderSettingsActivity.cpp | 19 ++++-- 6 files changed, 159 insertions(+), 23 deletions(-) diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index df91c1a1..eb9c3d65 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -102,14 +102,19 @@ STR_AUTHENTICATING: "Authenticating..." STR_AUTH_SUCCESS: "Successfully authenticated!" STR_KOREADER_AUTH: "KOReader Auth" STR_SYNC_READY: "KOReader sync is ready to use" -STR_AUTH_FAILED: "Authentication Failed" +STR_AUTH_FAILED: "Authentication failed" +STR_REGISTER: "Register" +STR_REGISTERING: "Registering..." +STR_REGISTER_SUCCESS: "Account created successfully!" +STR_REGISTER_FAILED: "Registration failed" +STR_USERNAME_TAKEN: "Username already taken" STR_DONE: "Done" STR_CLEAR_CACHE_WARNING_1: "This will clear all cached book data." STR_CLEAR_CACHE_WARNING_2: "All reading progress will be lost!" STR_CLEAR_CACHE_WARNING_3: "Books will need to be re-indexed" STR_CLEAR_CACHE_WARNING_4: "when opened again." STR_CLEARING_CACHE: "Clearing cache..." -STR_CACHE_CLEARED: "Cache Cleared" +STR_CACHE_CLEARED: "Cache cleared" STR_ITEMS_REMOVED: "items removed" STR_FAILED_LOWER: "failed" STR_CLEAR_CACHE_FAILED: "Failed to clear cache" diff --git a/lib/KOReaderSync/KOReaderSyncClient.cpp b/lib/KOReaderSync/KOReaderSyncClient.cpp index 66f7bee1..e25089f3 100644 --- a/lib/KOReaderSync/KOReaderSyncClient.cpp +++ b/lib/KOReaderSync/KOReaderSyncClient.cpp @@ -28,6 +28,67 @@ void addAuthHeaders(HTTPClient& http) { bool isHttpsUrl(const std::string& url) { return url.rfind("https://", 0) == 0; } } // namespace +KOReaderSyncClient::Error KOReaderSyncClient::registerUser() { + if (!KOREADER_STORE.hasCredentials()) { + LOG_DBG("KOSync", "No credentials configured"); + return NO_CREDENTIALS; + } + + std::string url = KOREADER_STORE.getBaseUrl() + "/users/create"; + LOG_DBG("KOSync", "Registering user: %s", url.c_str()); + + HTTPClient http; + std::unique_ptr secureClient; + WiFiClient plainClient; + + if (isHttpsUrl(url)) { + secureClient.reset(new WiFiClientSecure); + secureClient->setInsecure(); + http.begin(*secureClient, url.c_str()); + } else { + http.begin(plainClient, url.c_str()); + } + + http.addHeader("Accept", "application/vnd.koreader.v1+json"); + http.addHeader("Content-Type", "application/json"); + + JsonDocument doc; + doc["username"] = KOREADER_STORE.getUsername(); + doc["password"] = KOREADER_STORE.getMd5Password(); + std::string body; + serializeJson(doc, body); + + LOG_DBG("KOSync", "Register request body: "); + + const int httpCode = http.POST(body.c_str()); + const String responseBody = http.getString(); + http.end(); + + LOG_DBG("KOSync", "Register response: %d | body: %s", httpCode, responseBody.c_str()); + + if (httpCode == 201) { + return OK; + } else if (httpCode == 200) { + // Some server implementations return 200 when the user already exists + return USER_EXISTS; + } else if (httpCode == 402) { + // Both "user already exists" (error 2002) and "registration disabled" (error 2005) + // return HTTP 402 on the original kosync server. Distinguish them by body text. + String lowerBody = responseBody; + lowerBody.toLowerCase(); + if (lowerBody.indexOf("already") >= 0) { + return USER_EXISTS; + } + return REGISTRATION_DISABLED; + } else if (httpCode == 409) { + // korrosync returns 409 for existing users + return USER_EXISTS; + } else if (httpCode < 0) { + return NETWORK_ERROR; + } + return SERVER_ERROR; +} + KOReaderSyncClient::Error KOReaderSyncClient::authenticate() { if (!KOREADER_STORE.hasCredentials()) { LOG_DBG("KOSync", "No credentials configured"); @@ -195,6 +256,10 @@ const char* KOReaderSyncClient::errorString(Error error) { return "JSON parse error"; case NOT_FOUND: return "No progress found"; + case USER_EXISTS: + return "Username is already taken"; + case REGISTRATION_DISABLED: + return "Registration is disabled on this server"; default: return "Unknown error"; } diff --git a/lib/KOReaderSync/KOReaderSyncClient.h b/lib/KOReaderSync/KOReaderSyncClient.h index a9bc5c09..f6c53d32 100644 --- a/lib/KOReaderSync/KOReaderSyncClient.h +++ b/lib/KOReaderSync/KOReaderSyncClient.h @@ -19,9 +19,10 @@ struct KOReaderProgress { * Base URL: https://sync.koreader.rocks:443/ * * API Endpoints: - * GET /users/auth - Authenticate (validate credentials) - * GET /syncs/progress/:document - Get progress for a document - * PUT /syncs/progress - Update progress for a document + * POST /users/create - Register a new user + * GET /users/auth - Authenticate (validate credentials) + * GET /syncs/progress/:document - Get progress for a document + * PUT /syncs/progress - Update progress for a document * * Authentication: * x-auth-user: username @@ -29,7 +30,24 @@ struct KOReaderProgress { */ class KOReaderSyncClient { public: - enum Error { OK = 0, NO_CREDENTIALS, NETWORK_ERROR, AUTH_FAILED, SERVER_ERROR, JSON_ERROR, NOT_FOUND }; + enum Error { + OK = 0, + NO_CREDENTIALS, + NETWORK_ERROR, + AUTH_FAILED, + SERVER_ERROR, + JSON_ERROR, + NOT_FOUND, + USER_EXISTS, + REGISTRATION_DISABLED + }; + + /** + * Register a new user account with the sync server. + * Uses credentials already stored in KOReaderCredentialStore. + * @return OK on success, USER_EXISTS if taken, REGISTRATION_DISABLED if server disallows it + */ + static Error registerUser(); /** * Authenticate with the sync server (validate credentials). diff --git a/src/activities/settings/KOReaderAuthActivity.cpp b/src/activities/settings/KOReaderAuthActivity.cpp index 2240ac6e..2c2a0f81 100644 --- a/src/activities/settings/KOReaderAuthActivity.cpp +++ b/src/activities/settings/KOReaderAuthActivity.cpp @@ -24,12 +24,21 @@ void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) { { RenderLock lock(*this); - state = AUTHENTICATING; - statusMessage = tr(STR_AUTHENTICATING); + if (mode == Mode::REGISTER) { + state = REGISTERING; + statusMessage = tr(STR_REGISTERING); + } else { + state = AUTHENTICATING; + statusMessage = tr(STR_AUTHENTICATING); + } } requestUpdate(); - performAuthentication(); + if (mode == Mode::REGISTER) { + performRegistration(); + } else { + performAuthentication(); + } } void KOReaderAuthActivity::performAuthentication() { @@ -48,6 +57,25 @@ void KOReaderAuthActivity::performAuthentication() { requestUpdate(); } +void KOReaderAuthActivity::performRegistration() { + const auto result = KOReaderSyncClient::registerUser(); + + { + RenderLock lock(*this); + if (result == KOReaderSyncClient::OK) { + state = SUCCESS; + statusMessage = tr(STR_REGISTER_SUCCESS); + } else if (result == KOReaderSyncClient::USER_EXISTS) { + state = USER_EXISTS; + errorMessage = KOReaderSyncClient::errorString(result); + } else { + state = FAILED; + errorMessage = KOReaderSyncClient::errorString(result); + } + } + requestUpdate(); +} + void KOReaderAuthActivity::onEnter() { Activity::onEnter(); @@ -83,13 +111,18 @@ void KOReaderAuthActivity::render(RenderLock&&) { const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = (pageHeight - height) / 2; - if (state == AUTHENTICATING) { + if (state == AUTHENTICATING || state == REGISTERING) { renderer.drawCenteredText(UI_10_FONT_ID, top, statusMessage.c_str()); } else if (state == SUCCESS) { - renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_AUTH_SUCCESS), true, EpdFontFamily::BOLD); + const char* successMsg = (mode == Mode::REGISTER) ? tr(STR_REGISTER_SUCCESS) : tr(STR_AUTH_SUCCESS); + renderer.drawCenteredText(UI_10_FONT_ID, top, successMsg, true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, tr(STR_SYNC_READY)); + } else if (state == USER_EXISTS) { + renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_USERNAME_TAKEN), true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, errorMessage.c_str()); } else if (state == FAILED) { - renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_AUTH_FAILED), true, EpdFontFamily::BOLD); + const char* failedMsg = (mode == Mode::REGISTER) ? tr(STR_REGISTER_FAILED) : tr(STR_AUTH_FAILED); + renderer.drawCenteredText(UI_10_FONT_ID, top, failedMsg, true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, errorMessage.c_str()); } @@ -99,7 +132,7 @@ void KOReaderAuthActivity::render(RenderLock&&) { } void KOReaderAuthActivity::loop() { - if (state == SUCCESS || state == FAILED) { + if (state == SUCCESS || state == FAILED || state == USER_EXISTS) { if (mappedInput.wasPressed(MappedInputManager::Button::Back) || mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { finish(); diff --git a/src/activities/settings/KOReaderAuthActivity.h b/src/activities/settings/KOReaderAuthActivity.h index b4e330e7..b0b54b93 100644 --- a/src/activities/settings/KOReaderAuthActivity.h +++ b/src/activities/settings/KOReaderAuthActivity.h @@ -5,27 +5,31 @@ #include "activities/Activity.h" /** - * Activity for testing KOReader credentials. - * Connects to WiFi and authenticates with the KOReader sync server. + * Activity for authenticating or registering a KOReader sync account. + * Connects to WiFi, then either logs in or creates a new account on the server. */ class KOReaderAuthActivity final : public Activity { public: - explicit KOReaderAuthActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity("KOReaderAuth", renderer, mappedInput) {} + enum class Mode { LOGIN, REGISTER }; + + explicit KOReaderAuthActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, Mode mode = Mode::LOGIN) + : Activity("KOReaderAuth", renderer, mappedInput), mode(mode) {} void onEnter() override; void onExit() override; void loop() override; void render(RenderLock&&) override; - bool preventAutoSleep() override { return state == CONNECTING || state == AUTHENTICATING; } + bool preventAutoSleep() override { return state == CONNECTING || state == AUTHENTICATING || state == REGISTERING; } private: - enum State { WIFI_SELECTION, CONNECTING, AUTHENTICATING, SUCCESS, FAILED }; + enum State { WIFI_SELECTION, CONNECTING, AUTHENTICATING, REGISTERING, SUCCESS, FAILED, USER_EXISTS }; + Mode mode; State state = WIFI_SELECTION; std::string statusMessage; std::string errorMessage; void onWifiSelectionComplete(bool success); void performAuthentication(); + void performRegistration(); }; diff --git a/src/activities/settings/KOReaderSettingsActivity.cpp b/src/activities/settings/KOReaderSettingsActivity.cpp index 7291c8c1..391d2ee3 100644 --- a/src/activities/settings/KOReaderSettingsActivity.cpp +++ b/src/activities/settings/KOReaderSettingsActivity.cpp @@ -13,9 +13,9 @@ #include "fontIds.h" namespace { -constexpr int MENU_ITEMS = 5; -const StrId menuNames[MENU_ITEMS] = {StrId::STR_USERNAME, StrId::STR_PASSWORD, StrId::STR_SYNC_SERVER_URL, - StrId::STR_DOCUMENT_MATCHING, StrId::STR_AUTHENTICATE}; +constexpr int MENU_ITEMS = 6; +const StrId menuNames[MENU_ITEMS] = {StrId::STR_USERNAME, StrId::STR_PASSWORD, StrId::STR_SYNC_SERVER_URL, + StrId::STR_DOCUMENT_MATCHING, StrId::STR_AUTHENTICATE, StrId::STR_REGISTER}; } // namespace void KOReaderSettingsActivity::onEnter() { @@ -107,7 +107,16 @@ void KOReaderSettingsActivity::handleSelection() { // Can't authenticate without credentials - just show message briefly return; } - startActivityForResult(std::make_unique(renderer, mappedInput), [](const ActivityResult&) {}); + startActivityForResult(std::make_unique(renderer, mappedInput, KOReaderAuthActivity::Mode::LOGIN), + [](const ActivityResult&) {}); + } else if (selectedIndex == 5) { + // Register + if (!KOREADER_STORE.hasCredentials()) { + return; + } + startActivityForResult( + std::make_unique(renderer, mappedInput, KOReaderAuthActivity::Mode::REGISTER), + [](const ActivityResult&) {}); } } @@ -141,6 +150,8 @@ void KOReaderSettingsActivity::render(RenderLock&&) { : std::string(tr(STR_BINARY)); } else if (index == 4) { return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]"; + } else if (index == 5) { + return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]"; } return std::string(tr(STR_NOT_SET)); },