feat: Add kosync user registration and switch to crosspoint-sync server (#2587)

Implements createUser() endpoint to allow account creation via the
KOSync protocol. Changes default sync server from sync.koreader.rocks to
sync.crosspointreader.com with migration logic to preserve existing
users' server settings. Extends sync protocol to include position data
(spine index, page numbers, xpath) that crosspoint-sync supports while
remaining compatible with standard kosync servers.
This commit is contained in:
Justin Mitchell
2026-07-18 14:50:51 -04:00
committed by GitHub
parent 9737cb335c
commit b1d037569b
11 changed files with 320 additions and 53 deletions
+40 -25
View File
@@ -28,8 +28,10 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware control
- [3.6.5 OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries)
- [3.6.6 Web Settings (Wi-Fi + OPDS)](#366-web-settings-wi-fi--opds)
- [3.6.7 KOReader Sync Quick Setup](#367-koreader-sync-quick-setup)
- [Option A: Free Public Server (`sync.koreader.rocks`)](#option-a-free-public-server-synckoreaderrocks)
- [Option B: Self-Hosted Server (Docker Compose)](#option-b-self-hosted-server-docker-compose)
- [Option A: CrossPoint Sync Server (`sync.crosspointreader.com`, default)](#option-a-crosspoint-sync-server-synccrosspointreadercom-default)
- [Option B: Legacy Public KOReader Server (`sync.koreader.rocks`)](#option-b-legacy-public-koreader-server-synckoreaderrocks)
- [Option C: Self-Hosted Server (Docker Compose)](#option-c-self-hosted-server-docker-compose)
- [Syncing While Reading](#syncing-while-reading)
- [3.7 Sleep Screen](#37-sleep-screen)
- [Cover settings](#cover-settings)
- [Custom images](#custom-images)
@@ -360,9 +362,37 @@ Behavior notes:
CrossPoint can sync reading progress with KOReader-compatible sync servers.
It also interoperates with KOReader apps/devices when they use the same server and credentials.
##### Option A: Free Public Server (`sync.koreader.rocks`)
##### Option A: CrossPoint Sync Server (`sync.crosspointreader.com`, default)
1. Register a user once (only if needed):
When **Sync Server URL** is left empty, CrossPoint uses the free CrossPoint sync server at `https://sync.crosspointreader.com`. It speaks the standard KOReader sync protocol (so KOReader apps can use it too) and additionally stores an exact spine/page position for lossless CrossPoint-to-CrossPoint sync.
1. On each CrossPoint device:
- Go to **Settings -> System -> KOReader Sync**.
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
- Leave **Sync Server URL** empty (or set it to `https://sync.crosspointreader.com`).
- On the first device, run **Sign Up** once to create the account directly from the device. On every other device, just run **Authenticate**.
Accounts are per server. Existing `sync.koreader.rocks` credentials do not exist on the CrossPoint server; either sign up again with the same username/password or use Option B to keep using the legacy server.
##### Option B: Legacy Public KOReader Server (`sync.koreader.rocks`)
Use this if you already sync KOReader devices against the official public server.
1. On each CrossPoint device:
- Go to **Settings -> System -> KOReader Sync**.
- Set **Sync Server URL** to `https://sync.koreader.rocks` (required; an empty URL now points at the CrossPoint server instead).
- Set **Username** and **Password** to your existing KOReader Sync credentials.
- Run **Authenticate**.
2. If you do not have an account yet, run **Sign Up** on the device, or register once with curl:
```bash
USERNAME="user"
@@ -375,26 +405,9 @@ curl -i "https://sync.koreader.rocks/users/create" \
--data "{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD_MD5\"}"
```
Already have KOReader Sync credentials? Skip registration; basic sync only requires using the same existing username/password on all devices.
When this returns `HTTP 402` with `{"code":2002,"message":"Username is already registered."}`, pick a different username or use that existing account.
2. On each CrossPoint device:
- Go to **Settings -> System -> KOReader Sync**.
- Set **Username** and **Password** (enter the plain password; CrossPoint computes MD5 internally, and use the same values on all devices).
- Set **Sync Server URL** to `https://sync.koreader.rocks`, or leave it empty (both use the same default KOReader sync server).
- Run **Authenticate**.
3. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**. Alternatively, set **Settings -> Controls -> Long-press Menu** to **KOSync** and hold Confirm to launch sync directly.
- With **Sync Behavior** set to **Ask every time**, choose **Apply Remote** to jump to remote progress or **Upload Local** to push current progress.
- With **Sync Behavior** set to **Smart sync**, CrossPoint auto-resolves simple cases: upload when no remote progress exists, confirm and leave both unchanged when local and remote progress are already synchronized, upload when local progress is further ahead, or apply remote when remote progress is further ahead.
##### Option B: Self-Hosted Server (Docker Compose)
##### Option C: Self-Hosted Server (Docker Compose)
1. Start a sync server:
@@ -467,10 +480,12 @@ If this returns `HTTP 402` with `{"code":2002,"message":"Username is already reg
If you use the HTTPS listener, use `https://<server-ip>:7200` (`curl -k` only for self-signed certificate testing).
5. While reading, press **Confirm** to open the reader menu, then select **Sync Progress**. Alternatively, set **Settings -> Controls -> Long-press Menu** to **KOSync** and hold Confirm to launch sync directly.
##### Syncing While Reading
- With **Sync Behavior** set to **Ask every time**, choose **Apply Remote** to jump to remote progress or **Upload Local** to push current progress.
- With **Sync Behavior** set to **Smart sync**, CrossPoint auto-resolves simple cases: upload when no remote progress exists, confirm and leave both unchanged when local and remote progress are already synchronized, upload when local progress is further ahead, or apply remote when remote progress is further ahead.
Once any of the options above is set up, press **Confirm** while reading to open the reader menu, then select **Sync Progress**. Alternatively, set **Settings -> Controls -> Long-press Menu** to **KOSync** and hold Confirm to launch sync directly.
- With **Sync Behavior** set to **Ask every time**, choose **Apply Remote** to jump to remote progress or **Upload Local** to push current progress.
- With **Sync Behavior** set to **Smart sync**, CrossPoint auto-resolves simple cases: upload when no remote progress exists, confirm and leave both unchanged when local and remote progress are already synchronized, upload when local progress is further ahead, or apply remote when remote progress is further ahead.
### 3.7 Sleep Screen
+5
View File
@@ -113,6 +113,11 @@ STR_SYNC_BEHAVIOR: "Sync Behavior"
STR_ASK_EVERY_TIME: "Ask every time"
STR_SMART_SYNC: "Smart sync"
STR_AUTHENTICATE: "Authenticate"
STR_SIGN_UP: "Sign Up"
STR_CREATING_ACCOUNT: "Creating account..."
STR_ACCOUNT_CREATED: "Account created"
STR_SIGNUP_FAILED: "Sign up failed"
STR_USERNAME_TAKEN: "Username is already registered"
STR_KOREADER_USERNAME: "KOReader Username"
STR_KOREADER_PASSWORD: "KOReader Password"
STR_FILENAME: "Filename"
+25 -2
View File
@@ -5,11 +5,21 @@
#include <ObfuscationUtils.h>
namespace {
// Default sync server URL
constexpr char DEFAULT_SERVER_URL[] = "https://sync.koreader.rocks:443";
// Default sync server URL. crosspoint-sync speaks the full KOSync protocol, so
// pointing at any other kosync server (e.g. https://sync.koreader.rocks:443)
// still works via the custom server URL setting.
constexpr char DEFAULT_SERVER_URL[] = "https://sync.crosspointreader.com";
// Default before config version 2. Configs saved without a version stamp and an
// empty serverUrl were implicitly syncing here — they get pinned on upgrade.
constexpr char LEGACY_DEFAULT_SERVER_URL[] = "https://sync.koreader.rocks:443";
// Bumped when a change to defaults would alter behavior for existing configs.
constexpr uint8_t CONFIG_VERSION = 2;
} // namespace
void KOReaderCredentialStore::toJson(JsonDocument& doc) const {
doc["cfgVersion"] = CONFIG_VERSION;
doc["username"] = getUsername();
doc["password_obf"] = obfuscation::obfuscateToBase64(getPassword());
doc["serverUrl"] = getServerUrl();
@@ -27,6 +37,19 @@ bool KOReaderCredentialStore::fromJson(JsonVariantConst doc) {
setCredentials(user, pass);
setServerUrl(doc["serverUrl"] | "");
// The default server changed in config v2 (sync.koreader.rocks -> crosspoint-sync).
// A pre-v2 config with credentials and no explicit URL was actively syncing
// against the old default — pin that URL so the upgrade doesn't switch servers
// out from under the user. Fresh setups get the new default.
const uint8_t cfgVersion = doc["cfgVersion"] | (uint8_t)1;
if (cfgVersion < CONFIG_VERSION) {
if (getServerUrl().empty() && hasCredentials()) {
LOG_DBG("KRS", "Pre-v2 config used the old default server; pinning %s", LEGACY_DEFAULT_SERVER_URL);
setServerUrl(LEGACY_DEFAULT_SERVER_URL);
}
needsResave = true; // stamp cfgVersion so this migration runs once
}
uint8_t method = doc["matchMethod"] | (uint8_t)0;
if (method <= static_cast<uint8_t>(DocumentMatchMethod::BINARY)) {
setMatchMethod(static_cast<DocumentMatchMethod>(method));
+67
View File
@@ -78,6 +78,43 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() {
return SERVER_ERROR;
}
KOReaderSyncClient::Error KOReaderSyncClient::createUser() {
lastHttpCode = 0;
if (!KOREADER_STORE.hasCredentials()) {
LOG_DBG("KOSync", "No credentials configured");
return NO_CREDENTIALS;
}
const std::string url = KOREADER_STORE.getBaseUrl() + "/users/create";
LOG_DBG("KOSync", "Creating account: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap());
if (insufficientHeap()) return LOW_MEMORY;
JsonDocument doc;
doc["username"] = KOREADER_STORE.getUsername();
doc["password"] = KOREADER_STORE.getMd5Password();
std::string body;
serializeJson(doc, body);
freeink::SecureHttpClient http;
http.setInsecure();
if (!http.begin(url)) {
LOG_ERR("KOSync", "Bad URL: %s", url.c_str());
return NETWORK_ERROR;
}
http.addHeader("Accept", "application/vnd.koreader.v1+json");
http.addHeader("Content-Type", "application/json");
const int httpCode = http.sendRequest("POST", body);
http.end();
lastHttpCode = httpCode;
LOG_DBG("KOSync", "Create user response: %d", httpCode);
if (httpCode <= 0) return NETWORK_ERROR;
if (httpCode == 200 || httpCode == 201) return OK;
if (httpCode == 402) return USER_EXISTS;
return SERVER_ERROR;
}
KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& documentHash,
KOReaderProgress& outProgress) {
lastHttpCode = 0;
@@ -124,6 +161,24 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
outProgress.deviceId = doc["device_id"].as<std::string>();
outProgress.timestamp = doc["timestamp"].as<int64_t>();
// Extended crosspoint-sync field; absent on plain kosync servers.
outProgress.position.reset();
const JsonObjectConst pos = doc["position"].as<JsonObjectConst>();
if (!pos.isNull()) {
KOReaderRichPosition rich;
rich.pctQ = pos["pctQ"].as<uint32_t>();
rich.spineIndex = pos["spine"].as<uint16_t>();
rich.pageNumber = pos["page"].as<uint16_t>();
const uint16_t pages = pos["pages"].as<uint16_t>();
rich.totalPages = pages > 0 ? pages : 1;
const uint16_t para = pos["para"].as<uint16_t>();
if (para > 0) rich.paragraphIndex = para;
rich.xpath = pos["xpath"].as<const char*>() ? pos["xpath"].as<const char*>() : "";
LOG_DBG("KOSync", "Got rich position: spine=%u page=%u/%u para=%u", rich.spineIndex, rich.pageNumber,
rich.totalPages, para);
outProgress.position = std::move(rich);
}
LOG_DBG("KOSync", "Got progress: %.2f%% at %s", outProgress.percentage * 100, outProgress.progress.c_str());
return OK;
}
@@ -158,6 +213,18 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr
doc["percentage"] = progress.percentage;
doc["device"] = DEVICE_NAME;
doc["device_id"] = DEVICE_ID;
if (progress.position.has_value()) {
// Extended crosspoint-sync field; kosync servers ignore unknown keys.
const auto& p = *progress.position;
auto pos = doc["position"].to<JsonObject>();
pos["pctQ"] = p.pctQ;
pos["spine"] = p.spineIndex;
pos["page"] = p.pageNumber;
pos["pages"] = p.totalPages;
if (p.paragraphIndex.has_value()) pos["para"] = *p.paragraphIndex;
// Server rejects the whole position object if xpath exceeds 120 bytes.
if (!p.xpath.empty() && p.xpath.size() <= 120) pos["xpath"] = p.xpath;
}
std::string body;
serializeJson(doc, body);
+42 -8
View File
@@ -14,17 +14,33 @@ struct KOReaderMetadata {
std::string authors; // Author(s) from EPUB metadata
};
/**
* Rich CrossPoint position sent alongside progress uploads. Maps 1:1 onto the
* crosspoint-sync extended `position` object (see crosspoint-sync docs/API.md).
* The official KOSync server ignores unknown fields; crosspoint-sync stores it
* so CrossPoint<->CrossPoint sync is lossless instead of xpath-approximated.
*/
struct KOReaderRichPosition {
uint32_t pctQ = 0; // Percentage quantized 0..1,000,000 (authoritative)
uint16_t spineIndex = 0; // Spine (chapter) index
uint16_t pageNumber = 0; // Page within spine (layout-dependent hint)
uint16_t totalPages = 1; // Spine page count (layout-dependent hint)
std::optional<uint16_t> paragraphIndex; // Synthetic 1-based paragraph index
std::string xpath; // KOReader-style xpath (server cap: 120 bytes)
};
/**
* Progress data from KOReader sync server.
*/
struct KOReaderProgress {
std::string document; // Document hash
std::string progress; // XPath-like progress string
float percentage; // Progress percentage (0.0 to 1.0)
std::string device; // Device name
std::string deviceId; // Device ID
int64_t timestamp; // Unix timestamp of last update
std::optional<KOReaderMetadata> metadata; // Optional document metadata
std::string document; // Document hash
std::string progress; // XPath-like progress string
float percentage; // Progress percentage (0.0 to 1.0)
std::string device; // Device name
std::string deviceId; // Device ID
int64_t timestamp; // Unix timestamp of last update
std::optional<KOReaderMetadata> metadata; // Optional document metadata
std::optional<KOReaderRichPosition> position; // Optional rich position (crosspoint-sync servers only)
};
/**
@@ -43,7 +59,17 @@ struct KOReaderProgress {
*/
class KOReaderSyncClient {
public:
enum Error { OK = 0, NO_CREDENTIALS, NETWORK_ERROR, AUTH_FAILED, SERVER_ERROR, JSON_ERROR, NOT_FOUND, LOW_MEMORY };
enum Error {
OK = 0,
NO_CREDENTIALS,
NETWORK_ERROR,
AUTH_FAILED,
SERVER_ERROR,
JSON_ERROR,
NOT_FOUND,
LOW_MEMORY,
USER_EXISTS
};
/**
* Authenticate with the sync server (validate credentials).
@@ -51,6 +77,14 @@ class KOReaderSyncClient {
*/
static Error authenticate();
/**
* Register a new account on the sync server using the stored credentials
* (POST /users/create with the MD5 auth key — the server never sees the
* plain password).
* @return OK on success, USER_EXISTS if the username is taken
*/
static Error createUser();
/**
* Get reading progress for a document.
* @param documentHash The document hash (from KOReaderDocumentId)
+53
View File
@@ -724,6 +724,59 @@ SavedProgressPosition ProgressMapper::toSavedProgress(const std::shared_ptr<Epub
return result;
}
std::optional<CrossPointPosition> ProgressMapper::fromRichPosition(const std::shared_ptr<Epub>& epub,
const KOReaderRichPosition& rich,
GfxRenderer& renderer) {
const int spineCount = epub->getSpineItemsCount();
if (static_cast<int>(rich.spineIndex) >= spineCount) {
LOG_DBG("PM", "Rich position spine %u out of range (%d spine items)", rich.spineIndex, spineCount);
return std::nullopt;
}
CrossPointPosition result{};
result.spineIndex = rich.spineIndex;
Section tempSection(epub, result.spineIndex, renderer);
const auto cachedCount = tempSection.getCachedPageCount();
if (!cachedCount || *cachedCount <= 0) {
// No local layout for the target spine yet; the percentage/xpath mapping
// handles density estimation better than a blind copy of remote pages.
LOG_DBG("PM", "Rich position spine %u has no cached page count", rich.spineIndex);
return std::nullopt;
}
result.totalPages = *cachedCount;
const int remotePages = rich.totalPages > 0 ? rich.totalPages : 1;
if (result.totalPages == remotePages) {
// Identical layout (same render settings) — the page transfers losslessly.
result.pageNumber = std::min<int>(rich.pageNumber, result.totalPages - 1);
LOG_DBG("PM", "Rich position exact: spine=%d page=%d/%d", result.spineIndex, result.pageNumber, result.totalPages);
return result;
}
// Layout differs; the paragraph LUT is the most accurate anchor we have.
if (rich.paragraphIndex.has_value()) {
const auto lutPage = tempSection.getPageForParagraphIndex(*rich.paragraphIndex);
if (lutPage.has_value()) {
result.paragraphIndex = *rich.paragraphIndex;
result.hasParagraphIndex = true;
result.pageNumber = std::min<int>(*lutPage, result.totalPages - 1);
LOG_DBG("PM", "Rich position para %u -> spine=%d page=%d/%d", *rich.paragraphIndex, result.spineIndex,
result.pageNumber, result.totalPages);
return result;
}
}
// Fall back to the intra-spine page fraction.
const float intra =
(remotePages > 1) ? static_cast<float>(rich.pageNumber) / static_cast<float>(remotePages - 1) : 0.0f;
result.pageNumber = std::max(
0, std::min(static_cast<int>(intra * static_cast<float>(result.totalPages - 1) + 0.5f), result.totalPages - 1));
LOG_DBG("PM", "Rich position scaled: spine=%d remote %u/%d -> page=%d/%d", result.spineIndex, rich.pageNumber,
remotePages, result.pageNumber, result.totalPages);
return result;
}
CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr<Epub>& epub, const SavedProgressPosition& koPos,
GfxRenderer& renderer, int currentSpineIndex,
int totalPagesInCurrentSpine, int fallbackTotalPages) {
+17
View File
@@ -3,8 +3,11 @@
#include <GfxRenderer.h>
#include <memory>
#include <optional>
#include <string>
#include "KOReaderSyncClient.h"
/**
* CrossPoint position representation.
*/
@@ -65,6 +68,20 @@ class ProgressMapper {
GfxRenderer& renderer, int currentSpineIndex = -1,
int totalPagesInCurrentSpine = 0, int fallbackTotalPages = 0);
/**
* Convert a rich CrossPoint position (downloaded from a crosspoint-sync
* server) directly to a CrossPoint position, without XPath approximation.
* When the local layout matches the uploader's (same spine page count) the
* page transfers losslessly; otherwise the paragraph LUT or the intra-spine
* page fraction is used.
*
* @return The position, or std::nullopt when the rich position cannot be
* applied (spine out of range, no section cache) and the caller
* should fall back to toCrossPoint().
*/
static std::optional<CrossPointPosition> fromRichPosition(const std::shared_ptr<Epub>& epub,
const KOReaderRichPosition& rich, GfxRenderer& renderer);
private:
/**
* Generate a fallback XPath by streaming the spine item's XHTML and resolving
+29 -2
View File
@@ -232,8 +232,19 @@ void KOReaderSyncActivity::performSync() {
return;
}
SavedProgressPosition koPos = {remoteProgress.progress, remoteProgress.percentage};
remotePosition = ProgressMapper::toCrossPoint(epub, koPos, renderer, currentSpineIndex, totalPagesInSpine);
// Prefer the exact spine/page from a crosspoint-sync rich position (lossless
// CrossPoint<->CrossPoint sync); fall back to the approximate XPath mapping
// for plain kosync servers or when the rich position cannot be applied.
std::optional<CrossPointPosition> richMapped;
if (remoteProgress.position.has_value()) {
richMapped = ProgressMapper::fromRichPosition(epub, *remoteProgress.position, renderer);
}
if (richMapped.has_value()) {
remotePosition = *richMapped;
} else {
SavedProgressPosition koPos = {remoteProgress.progress, remoteProgress.percentage};
remotePosition = ProgressMapper::toCrossPoint(epub, koPos, renderer, currentSpineIndex, totalPagesInSpine);
}
if (smartSyncEnabled()) {
static constexpr float SAME_PROGRESS_EPSILON = 0.001f; // 0.1 percentage points
@@ -287,6 +298,22 @@ void KOReaderSyncActivity::performUpload() {
progress.progress = localProgress.xpath;
progress.percentage = localProgress.percentage;
// Rich CrossPoint position for crosspoint-sync servers (lossless
// CrossPoint<->CrossPoint sync); plain kosync servers ignore the extra field.
{
KOReaderRichPosition pos;
const float pct = localProgress.percentage < 0.0f ? 0.0f
: localProgress.percentage > 1.0f ? 1.0f
: localProgress.percentage;
pos.pctQ = static_cast<uint32_t>(pct * 1000000.0f + 0.5f);
pos.spineIndex = static_cast<uint16_t>(currentSpineIndex);
pos.pageNumber = static_cast<uint16_t>(currentPage);
pos.totalPages = static_cast<uint16_t>(totalPagesInSpine > 0 ? totalPagesInSpine : 1);
pos.paragraphIndex = currentParagraphIndex;
pos.xpath = localProgress.xpath;
progress.position = std::move(pos);
}
// Optionally include document metadata (KOReader PR #15306)
if (KOREADER_STORE.getSendMetadata()) {
// The Epub is released before the sync network calls and is only reloaded on the
@@ -26,7 +26,7 @@ void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) {
{
RenderLock lock(*this);
state = AUTHENTICATING;
statusMessage = tr(STR_AUTHENTICATING);
statusMessage = mode == Mode::SIGN_UP ? tr(STR_CREATING_ACCOUNT) : tr(STR_AUTHENTICATING);
}
requestUpdate();
@@ -34,16 +34,17 @@ void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) {
}
void KOReaderAuthActivity::performAuthentication() {
const auto result = KOReaderSyncClient::authenticate();
const auto result = mode == Mode::SIGN_UP ? KOReaderSyncClient::createUser() : KOReaderSyncClient::authenticate();
{
RenderLock lock(*this);
if (result == KOReaderSyncClient::OK) {
state = SUCCESS;
statusMessage = tr(STR_AUTH_SUCCESS);
statusMessage = mode == Mode::SIGN_UP ? tr(STR_ACCOUNT_CREATED) : tr(STR_AUTH_SUCCESS);
} else {
state = FAILED;
errorMessage = KOReaderSyncClient::errorString(result);
errorMessage =
result == KOReaderSyncClient::USER_EXISTS ? tr(STR_USERNAME_TAKEN) : KOReaderSyncClient::errorString(result);
}
}
requestUpdate();
@@ -80,17 +81,21 @@ void KOReaderAuthActivity::render(RenderLock&&) {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_KOREADER_AUTH));
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight},
mode == Mode::SIGN_UP ? tr(STR_SIGN_UP) : tr(STR_KOREADER_AUTH));
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height) / 2;
if (state == AUTHENTICATING) {
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);
renderer.drawCenteredText(UI_10_FONT_ID, top,
mode == Mode::SIGN_UP ? tr(STR_ACCOUNT_CREATED) : tr(STR_AUTH_SUCCESS), true,
EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, tr(STR_SYNC_READY));
} else if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_AUTH_FAILED), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, top, mode == Mode::SIGN_UP ? tr(STR_SIGNUP_FAILED) : tr(STR_AUTH_FAILED),
true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, errorMessage.c_str());
}
@@ -5,13 +5,16 @@
#include "activities/Activity.h"
/**
* Activity for testing KOReader credentials.
* Connects to WiFi and authenticates with the KOReader sync server.
* Activity for testing KOReader credentials, or — in sign-up mode — creating a
* new account on the sync server with the entered username/password.
* Connects to WiFi, then authenticates or registers.
*/
class KOReaderAuthActivity final : public Activity {
public:
explicit KOReaderAuthActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("KOReaderAuth", renderer, mappedInput) {}
enum class Mode { AUTHENTICATE, SIGN_UP };
explicit KOReaderAuthActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, Mode mode = Mode::AUTHENTICATE)
: Activity("KOReaderAuth", renderer, mappedInput), mode(mode) {}
void onEnter() override;
void onExit() override;
@@ -22,6 +25,7 @@ class KOReaderAuthActivity final : public Activity {
private:
enum State { WIFI_SELECTION, CONNECTING, AUTHENTICATING, SUCCESS, FAILED };
Mode mode = Mode::AUTHENTICATE;
State state = WIFI_SELECTION;
std::string statusMessage;
std::string errorMessage;
@@ -13,10 +13,10 @@
#include "fontIds.h"
namespace {
constexpr int MENU_ITEMS = 7;
constexpr int MENU_ITEMS = 8;
const StrId menuNames[MENU_ITEMS] = {StrId::STR_USERNAME, StrId::STR_PASSWORD, StrId::STR_SYNC_SERVER_URL,
StrId::STR_DOCUMENT_MATCHING, StrId::STR_SEND_METADATA, StrId::STR_SYNC_BEHAVIOR,
StrId::STR_AUTHENTICATE};
StrId::STR_SIGN_UP, StrId::STR_AUTHENTICATE};
} // namespace
void KOReaderSettingsActivity::onEnter() {
@@ -112,6 +112,14 @@ void KOReaderSettingsActivity::handleSelection() {
KOREADER_STORE.saveToFile();
requestUpdate();
} else if (selectedIndex == 6) {
// Sign Up - create a new account on the sync server with the entered credentials
if (!KOREADER_STORE.hasCredentials()) {
return;
}
startActivityForResult(
std::make_unique<KOReaderAuthActivity>(renderer, mappedInput, KOReaderAuthActivity::Mode::SIGN_UP),
[](const ActivityResult&) {});
} else if (selectedIndex == 7) {
// Authenticate
if (!KOREADER_STORE.hasCredentials()) {
// Can't authenticate without credentials - just show message briefly
@@ -145,7 +153,16 @@ void KOReaderSettingsActivity::render(RenderLock&&) {
return KOREADER_STORE.getPassword().empty() ? std::string(tr(STR_NOT_SET)) : std::string("******");
} else if (index == 2) {
auto serverUrl = KOREADER_STORE.getServerUrl();
return serverUrl.empty() ? std::string(tr(STR_DEFAULT_VALUE)) : serverUrl;
if (!serverUrl.empty()) {
return serverUrl;
}
// Show which server the default actually is, scheme stripped for space
std::string defaultUrl = KOREADER_STORE.getBaseUrl();
const auto schemeEnd = defaultUrl.find("://");
if (schemeEnd != std::string::npos) {
defaultUrl.erase(0, schemeEnd + 3);
}
return std::string(tr(STR_DEFAULT_VALUE)) + ": " + defaultUrl;
} else if (index == 3) {
return KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? std::string(tr(STR_FILENAME))
: std::string(tr(STR_BINARY));
@@ -154,7 +171,7 @@ void KOReaderSettingsActivity::render(RenderLock&&) {
} else if (index == 5) {
return KOREADER_STORE.getSyncBehavior() == KOReaderSyncBehavior::SMART ? std::string(tr(STR_SMART_SYNC))
: std::string(tr(STR_ASK_EVERY_TIME));
} else if (index == 6) {
} else if (index == 6 || index == 7) {
return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]";
}
return std::string(tr(STR_NOT_SET));