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
+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));