Files
crosspoint-reader-mod/src/activities/reader/KOReaderSyncActivity.cpp
Zach Nelson d02e21a48f
Some checks failed
CI (build) / clang-format (push) Has been cancelled
CI (build) / cppcheck (push) Has been cancelled
CI (build) / build (push) Has been cancelled
CI (build) / Test Status (push) Has been cancelled
fix: Added missing up/down button labels (#935)
## Summary

**What is the goal of this PR?**

In some places, button labels are omitted intentionally because the
button has no purpose in the activity. I noticed a few obvious cases,
like Home > File Transfer and Settings > System > Language, where the up
and down button labels were missing. This change fixes those and all
similar instances I could find.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
2026-02-18 21:54:02 +03:00

410 lines
12 KiB
C++

#include "KOReaderSyncActivity.h"
#include <GfxRenderer.h>
#include <I18n.h>
#include <Logging.h>
#include <WiFi.h>
#include <esp_sntp.h>
#include "KOReaderCredentialStore.h"
#include "KOReaderDocumentId.h"
#include "MappedInputManager.h"
#include "activities/network/WifiSelectionActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
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");
esp_sntp_init();
// Wait for time to sync (with timeout)
int retry = 0;
const int maxRetries = 50; // 5 seconds max
while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED && retry < maxRetries) {
vTaskDelay(100 / portTICK_PERIOD_MS);
retry++;
}
if (retry < maxRetries) {
LOG_DBG("KOSync", "NTP time synced");
} else {
LOG_DBG("KOSync", "NTP sync timeout, using fallback");
}
}
} // namespace
void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
exitActivity();
if (!success) {
LOG_DBG("KOSync", "WiFi connection failed, exiting");
onCancel();
return;
}
LOG_DBG("KOSync", "WiFi connected, starting sync");
{
RenderLock lock(*this);
state = SYNCING;
statusMessage = tr(STR_SYNCING_TIME);
}
requestUpdate();
// Sync time with NTP before making API requests
syncTimeWithNTP();
{
RenderLock lock(*this);
statusMessage = tr(STR_CALC_HASH);
}
requestUpdate();
performSync();
}
void KOReaderSyncActivity::performSync() {
// 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()) {
{
RenderLock lock(*this);
state = SYNC_FAILED;
statusMessage = tr(STR_HASH_FAILED);
}
requestUpdate();
return;
}
LOG_DBG("KOSync", "Document hash: %s", documentHash.c_str());
{
RenderLock lock(*this);
statusMessage = tr(STR_FETCH_PROGRESS);
}
requestUpdateAndWait();
// Fetch remote progress
const auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress);
if (result == KOReaderSyncClient::NOT_FOUND) {
// No remote progress - offer to upload
{
RenderLock lock(*this);
state = NO_REMOTE_PROGRESS;
hasRemoteProgress = false;
}
requestUpdate();
return;
}
if (result != KOReaderSyncClient::OK) {
{
RenderLock lock(*this);
state = SYNC_FAILED;
statusMessage = KOReaderSyncClient::errorString(result);
}
requestUpdate();
return;
}
// Convert remote progress to CrossPoint position
hasRemoteProgress = true;
KOReaderPosition koPos = {remoteProgress.progress, remoteProgress.percentage};
remotePosition = ProgressMapper::toCrossPoint(epub, koPos, totalPagesInSpine);
// Calculate local progress in KOReader format (for display)
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine};
localProgress = ProgressMapper::toKOReader(epub, localPos);
{
RenderLock lock(*this);
state = SHOWING_RESULT;
selectedOption = 0; // Default to "Apply"
}
requestUpdate();
}
void KOReaderSyncActivity::performUpload() {
{
RenderLock lock(*this);
state = UPLOADING;
statusMessage = tr(STR_UPLOAD_PROGRESS);
}
requestUpdate();
requestUpdateAndWait();
// Convert current position to KOReader format
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine};
KOReaderPosition koPos = ProgressMapper::toKOReader(epub, localPos);
KOReaderProgress progress;
progress.document = documentHash;
progress.progress = koPos.xpath;
progress.percentage = koPos.percentage;
const auto result = KOReaderSyncClient::updateProgress(progress);
if (result != KOReaderSyncClient::OK) {
{
RenderLock lock(*this);
state = SYNC_FAILED;
statusMessage = KOReaderSyncClient::errorString(result);
}
requestUpdate();
return;
}
{
RenderLock lock(*this);
state = UPLOAD_COMPLETE;
}
requestUpdate();
}
void KOReaderSyncActivity::onEnter() {
ActivityWithSubactivity::onEnter();
// Check for credentials first
if (!KOREADER_STORE.hasCredentials()) {
state = NO_CREDENTIALS;
requestUpdate();
return;
}
// Turn on WiFi
LOG_DBG("KOSync", "Turning on WiFi...");
WiFi.mode(WIFI_STA);
// Check if already connected
if (WiFi.status() == WL_CONNECTED) {
LOG_DBG("KOSync", "Already connected to WiFi");
state = SYNCING;
statusMessage = tr(STR_SYNCING_TIME);
requestUpdate();
// Perform sync directly (will be handled in loop)
xTaskCreate(
[](void* param) {
auto* self = static_cast<KOReaderSyncActivity*>(param);
// Sync time first
syncTimeWithNTP();
{
RenderLock lock(*self);
self->statusMessage = tr(STR_CALC_HASH);
}
self->requestUpdate();
self->performSync();
vTaskDelete(nullptr);
},
"SyncTask", 4096, this, 1, nullptr);
return;
}
// Launch WiFi selection subactivity
LOG_DBG("KOSync", "Launching WifiSelectionActivity...");
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
[this](const bool connected) { onWifiSelectionComplete(connected); }));
}
void KOReaderSyncActivity::onExit() {
ActivityWithSubactivity::onExit();
// Turn off wifi
WiFi.disconnect(false);
delay(100);
WiFi.mode(WIFI_OFF);
delay(100);
}
void KOReaderSyncActivity::render(Activity::RenderLock&&) {
if (subActivity) {
return;
}
const auto pageWidth = renderer.getScreenWidth();
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_KOREADER_SYNC), true, EpdFontFamily::BOLD);
if (state == NO_CREDENTIALS) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_NO_CREDENTIALS_MSG), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, tr(STR_KOREADER_SETUP_HINT));
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
}
if (state == SYNCING || state == UPLOADING) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, statusMessage.c_str(), true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return;
}
if (state == SHOWING_RESULT) {
// Show comparison
renderer.drawCenteredText(UI_10_FONT_ID, 120, tr(STR_PROGRESS_FOUND), true, EpdFontFamily::BOLD);
// Get chapter names from TOC
const int remoteTocIndex = epub->getTocIndexForSpineIndex(remotePosition.spineIndex);
const int localTocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
const std::string remoteChapter =
(remoteTocIndex >= 0) ? epub->getTocItem(remoteTocIndex).title
: (std::string(tr(STR_SECTION_PREFIX)) + std::to_string(remotePosition.spineIndex + 1));
const std::string localChapter =
(localTocIndex >= 0) ? epub->getTocItem(localTocIndex).title
: (std::string(tr(STR_SECTION_PREFIX)) + std::to_string(currentSpineIndex + 1));
// Remote progress - chapter and page
renderer.drawText(UI_10_FONT_ID, 20, 160, tr(STR_REMOTE_LABEL), true);
char remoteChapterStr[128];
snprintf(remoteChapterStr, sizeof(remoteChapterStr), " %s", remoteChapter.c_str());
renderer.drawText(UI_10_FONT_ID, 20, 185, remoteChapterStr);
char remotePageStr[64];
snprintf(remotePageStr, sizeof(remotePageStr), tr(STR_PAGE_OVERALL_FORMAT), remotePosition.pageNumber + 1,
remoteProgress.percentage * 100);
renderer.drawText(UI_10_FONT_ID, 20, 210, remotePageStr);
if (!remoteProgress.device.empty()) {
char deviceStr[64];
snprintf(deviceStr, sizeof(deviceStr), tr(STR_DEVICE_FROM_FORMAT), remoteProgress.device.c_str());
renderer.drawText(UI_10_FONT_ID, 20, 235, deviceStr);
}
// Local progress - chapter and page
renderer.drawText(UI_10_FONT_ID, 20, 270, tr(STR_LOCAL_LABEL), true);
char localChapterStr[128];
snprintf(localChapterStr, sizeof(localChapterStr), " %s", localChapter.c_str());
renderer.drawText(UI_10_FONT_ID, 20, 295, localChapterStr);
char localPageStr[64];
snprintf(localPageStr, sizeof(localPageStr), tr(STR_PAGE_TOTAL_OVERALL_FORMAT), currentPage + 1, totalPagesInSpine,
localProgress.percentage * 100);
renderer.drawText(UI_10_FONT_ID, 20, 320, localPageStr);
const int optionY = 350;
const int optionHeight = 30;
// Apply option
if (selectedOption == 0) {
renderer.fillRect(0, optionY - 2, pageWidth - 1, optionHeight);
}
renderer.drawText(UI_10_FONT_ID, 20, optionY, tr(STR_APPLY_REMOTE), selectedOption != 0);
// Upload option
if (selectedOption == 1) {
renderer.fillRect(0, optionY + optionHeight - 2, pageWidth - 1, optionHeight);
}
renderer.drawText(UI_10_FONT_ID, 20, optionY + optionHeight, tr(STR_UPLOAD_LOCAL), selectedOption != 1);
// Bottom button hints
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
}
if (state == NO_REMOTE_PROGRESS) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_NO_REMOTE_MSG), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, tr(STR_UPLOAD_PROMPT));
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_UPLOAD), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
}
if (state == UPLOAD_COMPLETE) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_UPLOAD_SUCCESS), true, EpdFontFamily::BOLD);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
}
if (state == SYNC_FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_SYNC_FAILED_MSG), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, statusMessage.c_str());
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
}
}
void KOReaderSyncActivity::loop() {
if (subActivity) {
subActivity->loop();
return;
}
if (state == NO_CREDENTIALS || state == SYNC_FAILED || state == UPLOAD_COMPLETE) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onCancel();
}
return;
}
if (state == SHOWING_RESULT) {
// Navigate options
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
selectedOption = (selectedOption + 1) % 2; // Wrap around among 2 options
requestUpdate();
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
selectedOption = (selectedOption + 1) % 2; // Wrap around among 2 options
requestUpdate();
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (selectedOption == 0) {
// Apply remote progress
onSyncComplete(remotePosition.spineIndex, remotePosition.pageNumber);
} else if (selectedOption == 1) {
// Upload local progress
performUpload();
}
}
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onCancel();
}
return;
}
if (state == NO_REMOTE_PROGRESS) {
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
// Calculate hash if not done yet
if (documentHash.empty()) {
if (KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME) {
documentHash = KOReaderDocumentId::calculateFromFilename(epubPath);
} else {
documentHash = KOReaderDocumentId::calculate(epubPath);
}
}
performUpload();
}
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onCancel();
}
return;
}
}