initial version of custom fonts
This commit is contained in:
@@ -2,8 +2,6 @@
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#include "KOReaderCredentialStore.h"
|
||||
#include "KOReaderSyncActivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
@@ -12,7 +10,7 @@ namespace {
|
||||
constexpr int SKIP_PAGE_MS = 700;
|
||||
} // namespace
|
||||
|
||||
bool EpubReaderChapterSelectionActivity::hasSyncOption() const { return KOREADER_STORE.hasCredentials(); }
|
||||
bool EpubReaderChapterSelectionActivity::hasSyncOption() const { return false; }
|
||||
|
||||
int EpubReaderChapterSelectionActivity::getTotalItems() const {
|
||||
// Add 2 for sync options (top and bottom) if credentials are configured
|
||||
@@ -96,21 +94,7 @@ void EpubReaderChapterSelectionActivity::onExit() {
|
||||
}
|
||||
|
||||
void EpubReaderChapterSelectionActivity::launchSyncActivity() {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
exitActivity();
|
||||
enterNewActivity(new KOReaderSyncActivity(
|
||||
renderer, mappedInput, epub, epubPath, currentSpineIndex, currentPage, totalPagesInSpine,
|
||||
[this]() {
|
||||
// On cancel
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
},
|
||||
[this](int newSpineIndex, int newPage) {
|
||||
// On sync complete
|
||||
exitActivity();
|
||||
onSyncPosition(newSpineIndex, newPage);
|
||||
}));
|
||||
xSemaphoreGive(renderingMutex);
|
||||
// KOReader sync functionality removed
|
||||
}
|
||||
|
||||
void EpubReaderChapterSelectionActivity::loop() {
|
||||
|
||||
@@ -1,439 +0,0 @@
|
||||
#include "KOReaderSyncActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <WiFi.h>
|
||||
#include <esp_sntp.h>
|
||||
|
||||
#include "KOReaderCredentialStore.h"
|
||||
#include "KOReaderDocumentId.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "activities/network/WifiSelectionActivity.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) {
|
||||
Serial.printf("[%lu] [KOSync] NTP time synced\n", millis());
|
||||
} else {
|
||||
Serial.printf("[%lu] [KOSync] NTP sync timeout, using fallback\n", millis());
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void KOReaderSyncActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<KOReaderSyncActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
|
||||
exitActivity();
|
||||
|
||||
if (!success) {
|
||||
Serial.printf("[%lu] [KOSync] WiFi connection failed, exiting\n", millis());
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [KOSync] WiFi connected, starting sync\n", millis());
|
||||
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
state = SYNCING;
|
||||
statusMessage = "Syncing time...";
|
||||
xSemaphoreGive(renderingMutex);
|
||||
updateRequired = true;
|
||||
|
||||
// Sync time with NTP before making API requests
|
||||
syncTimeWithNTP();
|
||||
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
statusMessage = "Calculating document hash...";
|
||||
xSemaphoreGive(renderingMutex);
|
||||
updateRequired = true;
|
||||
|
||||
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()) {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
state = SYNC_FAILED;
|
||||
statusMessage = "Failed to calculate document hash";
|
||||
xSemaphoreGive(renderingMutex);
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [KOSync] Document hash: %s\n", millis(), documentHash.c_str());
|
||||
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
statusMessage = "Fetching remote progress...";
|
||||
xSemaphoreGive(renderingMutex);
|
||||
updateRequired = true;
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
|
||||
// Fetch remote progress
|
||||
const auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress);
|
||||
|
||||
if (result == KOReaderSyncClient::NOT_FOUND) {
|
||||
// No remote progress - offer to upload
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
state = NO_REMOTE_PROGRESS;
|
||||
hasRemoteProgress = false;
|
||||
xSemaphoreGive(renderingMutex);
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (result != KOReaderSyncClient::OK) {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
state = SYNC_FAILED;
|
||||
statusMessage = KOReaderSyncClient::errorString(result);
|
||||
xSemaphoreGive(renderingMutex);
|
||||
updateRequired = true;
|
||||
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);
|
||||
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
state = SHOWING_RESULT;
|
||||
selectedOption = 0; // Default to "Apply"
|
||||
xSemaphoreGive(renderingMutex);
|
||||
updateRequired = true;
|
||||
}
|
||||
|
||||
void KOReaderSyncActivity::performUpload() {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
state = UPLOADING;
|
||||
statusMessage = "Uploading progress...";
|
||||
xSemaphoreGive(renderingMutex);
|
||||
updateRequired = true;
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
|
||||
// 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) {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
state = SYNC_FAILED;
|
||||
statusMessage = KOReaderSyncClient::errorString(result);
|
||||
xSemaphoreGive(renderingMutex);
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
state = UPLOAD_COMPLETE;
|
||||
xSemaphoreGive(renderingMutex);
|
||||
updateRequired = true;
|
||||
}
|
||||
|
||||
void KOReaderSyncActivity::onEnter() {
|
||||
ActivityWithSubactivity::onEnter();
|
||||
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
|
||||
xTaskCreate(&KOReaderSyncActivity::taskTrampoline, "KOSyncTask",
|
||||
4096, // Stack size (larger for network operations)
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&displayTaskHandle // Task handle
|
||||
);
|
||||
|
||||
// Check for credentials first
|
||||
if (!KOREADER_STORE.hasCredentials()) {
|
||||
state = NO_CREDENTIALS;
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Turn on WiFi
|
||||
Serial.printf("[%lu] [KOSync] Turning on WiFi...\n", millis());
|
||||
WiFi.mode(WIFI_STA);
|
||||
|
||||
// Check if already connected
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.printf("[%lu] [KOSync] Already connected to WiFi\n", millis());
|
||||
state = SYNCING;
|
||||
statusMessage = "Syncing time...";
|
||||
updateRequired = true;
|
||||
|
||||
// Perform sync directly (will be handled in loop)
|
||||
xTaskCreate(
|
||||
[](void* param) {
|
||||
auto* self = static_cast<KOReaderSyncActivity*>(param);
|
||||
// Sync time first
|
||||
syncTimeWithNTP();
|
||||
xSemaphoreTake(self->renderingMutex, portMAX_DELAY);
|
||||
self->statusMessage = "Calculating document hash...";
|
||||
xSemaphoreGive(self->renderingMutex);
|
||||
self->updateRequired = true;
|
||||
self->performSync();
|
||||
vTaskDelete(nullptr);
|
||||
},
|
||||
"SyncTask", 4096, this, 1, nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
// Launch WiFi selection subactivity
|
||||
Serial.printf("[%lu] [KOSync] Launching WifiSelectionActivity...\n", millis());
|
||||
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);
|
||||
|
||||
// Wait until not rendering to delete task
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
vTaskDelete(displayTaskHandle);
|
||||
displayTaskHandle = nullptr;
|
||||
}
|
||||
vSemaphoreDelete(renderingMutex);
|
||||
renderingMutex = nullptr;
|
||||
}
|
||||
|
||||
void KOReaderSyncActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired) {
|
||||
updateRequired = false;
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
render();
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void KOReaderSyncActivity::render() {
|
||||
if (subActivity) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
|
||||
renderer.clearScreen();
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, "KOReader Sync", true, EpdFontFamily::BOLD);
|
||||
|
||||
if (state == NO_CREDENTIALS) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 280, "No credentials configured", true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 320, "Set up KOReader account in Settings");
|
||||
|
||||
const auto labels = mappedInput.mapLabels("Back", "", "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, 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, "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
|
||||
: ("Section " + std::to_string(remotePosition.spineIndex + 1));
|
||||
const std::string localChapter = (localTocIndex >= 0) ? epub->getTocItem(localTocIndex).title
|
||||
: ("Section " + std::to_string(currentSpineIndex + 1));
|
||||
|
||||
// Remote progress - chapter and page
|
||||
renderer.drawText(UI_10_FONT_ID, 20, 160, "Remote:", 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), " Page %d, %.2f%% overall", 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), " From: %s", 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, "Local:", 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), " Page %d/%d, %.2f%% overall", currentPage + 1, totalPagesInSpine,
|
||||
localProgress.percentage * 100);
|
||||
renderer.drawText(UI_10_FONT_ID, 20, 320, localPageStr);
|
||||
|
||||
// Options
|
||||
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, "Apply remote progress", 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, "Upload local progress", selectedOption != 1);
|
||||
|
||||
// Cancel option
|
||||
if (selectedOption == 2) {
|
||||
renderer.fillRect(0, optionY + optionHeight * 2 - 2, pageWidth - 1, optionHeight);
|
||||
}
|
||||
renderer.drawText(UI_10_FONT_ID, 20, optionY + optionHeight * 2, "Cancel", selectedOption != 2);
|
||||
|
||||
const auto labels = mappedInput.mapLabels("", "Select", "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == NO_REMOTE_PROGRESS) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 280, "No remote progress found", true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 320, "Upload current position?");
|
||||
|
||||
const auto labels = mappedInput.mapLabels("Cancel", "Upload", "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == UPLOAD_COMPLETE) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 300, "Progress uploaded!", true, EpdFontFamily::BOLD);
|
||||
|
||||
const auto labels = mappedInput.mapLabels("Back", "", "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == SYNC_FAILED) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 280, "Sync failed", true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 320, statusMessage.c_str());
|
||||
|
||||
const auto labels = mappedInput.mapLabels("Back", "", "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, 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 + 2) % 3; // Wrap around
|
||||
updateRequired = true;
|
||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
||||
selectedOption = (selectedOption + 1) % 3;
|
||||
updateRequired = true;
|
||||
}
|
||||
|
||||
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();
|
||||
} else {
|
||||
// Cancel
|
||||
onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
#pragma once
|
||||
#include <Epub.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include "KOReaderSyncClient.h"
|
||||
#include "ProgressMapper.h"
|
||||
#include "activities/ActivityWithSubactivity.h"
|
||||
|
||||
/**
|
||||
* Activity for syncing reading progress with KOReader sync server.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Connect to WiFi (if not connected)
|
||||
* 2. Calculate document hash
|
||||
* 3. Fetch remote progress
|
||||
* 4. Show comparison and options (Apply/Upload/Cancel)
|
||||
* 5. Apply or upload progress
|
||||
*/
|
||||
class KOReaderSyncActivity final : public ActivityWithSubactivity {
|
||||
public:
|
||||
using OnCancelCallback = std::function<void()>;
|
||||
using OnSyncCompleteCallback = std::function<void(int newSpineIndex, int newPageNumber)>;
|
||||
|
||||
explicit KOReaderSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::shared_ptr<Epub>& epub, const std::string& epubPath, int currentSpineIndex,
|
||||
int currentPage, int totalPagesInSpine, OnCancelCallback onCancel,
|
||||
OnSyncCompleteCallback onSyncComplete)
|
||||
: ActivityWithSubactivity("KOReaderSync", renderer, mappedInput),
|
||||
epub(epub),
|
||||
epubPath(epubPath),
|
||||
currentSpineIndex(currentSpineIndex),
|
||||
currentPage(currentPage),
|
||||
totalPagesInSpine(totalPagesInSpine),
|
||||
remoteProgress{},
|
||||
remotePosition{},
|
||||
localProgress{},
|
||||
onCancel(std::move(onCancel)),
|
||||
onSyncComplete(std::move(onSyncComplete)) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
bool preventAutoSleep() override { return state == CONNECTING || state == SYNCING; }
|
||||
|
||||
private:
|
||||
enum State {
|
||||
WIFI_SELECTION,
|
||||
CONNECTING,
|
||||
SYNCING,
|
||||
SHOWING_RESULT,
|
||||
UPLOADING,
|
||||
UPLOAD_COMPLETE,
|
||||
NO_REMOTE_PROGRESS,
|
||||
SYNC_FAILED,
|
||||
NO_CREDENTIALS
|
||||
};
|
||||
|
||||
std::shared_ptr<Epub> epub;
|
||||
std::string epubPath;
|
||||
int currentSpineIndex;
|
||||
int currentPage;
|
||||
int totalPagesInSpine;
|
||||
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
bool updateRequired = false;
|
||||
|
||||
State state = WIFI_SELECTION;
|
||||
std::string statusMessage;
|
||||
std::string documentHash;
|
||||
|
||||
// Remote progress data
|
||||
bool hasRemoteProgress = false;
|
||||
KOReaderProgress remoteProgress;
|
||||
CrossPointPosition remotePosition;
|
||||
|
||||
// Local progress as KOReader format (for display)
|
||||
KOReaderPosition localProgress;
|
||||
|
||||
// Selection in result screen (0=Apply, 1=Upload, 2=Cancel)
|
||||
int selectedOption = 0;
|
||||
|
||||
OnCancelCallback onCancel;
|
||||
OnSyncCompleteCallback onSyncComplete;
|
||||
|
||||
void onWifiSelectionComplete(bool success);
|
||||
void performSync();
|
||||
void performUpload();
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void render();
|
||||
};
|
||||
@@ -8,7 +8,6 @@
|
||||
#include "CalibreSettingsActivity.h"
|
||||
#include "ClearCacheActivity.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "KOReaderSettingsActivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "OtaUpdateActivity.h"
|
||||
#include "fontIds.h"
|
||||
@@ -18,6 +17,33 @@ void CategorySettingsActivity::taskTrampoline(void* param) {
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
int CategorySettingsActivity::getVisibleSettingsCount() const {
|
||||
int count = 0;
|
||||
for (int i = 0; i < settingsCount; i++) {
|
||||
if (settingsList[i].shouldShow()) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int CategorySettingsActivity::getActualIndex(int visibleIndex) const {
|
||||
int visibleCount = 0;
|
||||
for (int i = 0; i < settingsCount; i++) {
|
||||
if (settingsList[i].shouldShow()) {
|
||||
if (visibleCount == visibleIndex) {
|
||||
return i;
|
||||
}
|
||||
visibleCount++;
|
||||
}
|
||||
}
|
||||
return 0; // Fallback
|
||||
}
|
||||
|
||||
const SettingInfo& CategorySettingsActivity::getVisibleSetting(int visibleIndex) const {
|
||||
return settingsList[getActualIndex(visibleIndex)];
|
||||
}
|
||||
|
||||
void CategorySettingsActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
@@ -61,24 +87,28 @@ void CategorySettingsActivity::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle navigation
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
||||
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1);
|
||||
updateRequired = true;
|
||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
||||
selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0;
|
||||
updateRequired = true;
|
||||
// Handle navigation (using visible settings count)
|
||||
const int visibleCount = getVisibleSettingsCount();
|
||||
if (visibleCount > 0) {
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
||||
selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (visibleCount - 1);
|
||||
updateRequired = true;
|
||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
||||
selectedSettingIndex = (selectedSettingIndex < visibleCount - 1) ? (selectedSettingIndex + 1) : 0;
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CategorySettingsActivity::toggleCurrentSetting() {
|
||||
if (selectedSettingIndex < 0 || selectedSettingIndex >= settingsCount) {
|
||||
const int visibleCount = getVisibleSettingsCount();
|
||||
if (selectedSettingIndex < 0 || selectedSettingIndex >= visibleCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& setting = settingsList[selectedSettingIndex];
|
||||
const auto& setting = getVisibleSetting(selectedSettingIndex);
|
||||
|
||||
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {
|
||||
// Toggle the boolean value using the member pointer
|
||||
@@ -95,15 +125,7 @@ void CategorySettingsActivity::toggleCurrentSetting() {
|
||||
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
|
||||
}
|
||||
} else if (setting.type == SettingType::ACTION) {
|
||||
if (strcmp(setting.name, "KOReader Sync") == 0) {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
exitActivity();
|
||||
enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] {
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
}));
|
||||
xSemaphoreGive(renderingMutex);
|
||||
} else if (strcmp(setting.name, "Calibre Settings") == 0) {
|
||||
if (strcmp(setting.name, "Calibre Settings") == 0) {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
exitActivity();
|
||||
enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] {
|
||||
@@ -158,10 +180,15 @@ void CategorySettingsActivity::render() const {
|
||||
// Draw selection highlight
|
||||
renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30);
|
||||
|
||||
// Draw all settings
|
||||
// Draw only visible settings
|
||||
int visibleIndex = 0;
|
||||
for (int i = 0; i < settingsCount; i++) {
|
||||
const int settingY = 60 + i * 30; // 30 pixels between settings
|
||||
const bool isSelected = (i == selectedSettingIndex);
|
||||
if (!settingsList[i].shouldShow()) {
|
||||
continue; // Skip hidden settings
|
||||
}
|
||||
|
||||
const int settingY = 60 + visibleIndex * 30; // 30 pixels between settings
|
||||
const bool isSelected = (visibleIndex == selectedSettingIndex);
|
||||
|
||||
// Draw setting name
|
||||
renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, !isSelected);
|
||||
@@ -173,7 +200,9 @@ void CategorySettingsActivity::render() const {
|
||||
valueText = value ? "ON" : "OFF";
|
||||
} else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) {
|
||||
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr);
|
||||
valueText = settingsList[i].enumValues[value];
|
||||
if (value < settingsList[i].enumValues.size()) {
|
||||
valueText = settingsList[i].enumValues[value];
|
||||
}
|
||||
} else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) {
|
||||
valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr));
|
||||
}
|
||||
@@ -181,6 +210,8 @@ void CategorySettingsActivity::render() const {
|
||||
const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
|
||||
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), !isSelected);
|
||||
}
|
||||
|
||||
visibleIndex++;
|
||||
}
|
||||
|
||||
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
|
||||
|
||||
@@ -26,18 +26,29 @@ struct SettingInfo {
|
||||
};
|
||||
ValueRange valueRange;
|
||||
|
||||
static SettingInfo Toggle(const char* name, uint8_t CrossPointSettings::* ptr) {
|
||||
return {name, SettingType::TOGGLE, ptr};
|
||||
// Optional visibility callback - if set and returns false, setting is hidden
|
||||
std::function<bool()> isVisible;
|
||||
|
||||
// Check if this setting should be displayed
|
||||
bool shouldShow() const { return !isVisible || isVisible(); }
|
||||
|
||||
static SettingInfo Toggle(const char* name, uint8_t CrossPointSettings::* ptr,
|
||||
std::function<bool()> visible = nullptr) {
|
||||
return {name, SettingType::TOGGLE, ptr, {}, {}, std::move(visible)};
|
||||
}
|
||||
|
||||
static SettingInfo Enum(const char* name, uint8_t CrossPointSettings::* ptr, std::vector<std::string> values) {
|
||||
return {name, SettingType::ENUM, ptr, std::move(values)};
|
||||
static SettingInfo Enum(const char* name, uint8_t CrossPointSettings::* ptr, std::vector<std::string> values,
|
||||
std::function<bool()> visible = nullptr) {
|
||||
return {name, SettingType::ENUM, ptr, std::move(values), {}, std::move(visible)};
|
||||
}
|
||||
|
||||
static SettingInfo Action(const char* name) { return {name, SettingType::ACTION, nullptr}; }
|
||||
static SettingInfo Action(const char* name, std::function<bool()> visible = nullptr) {
|
||||
return {name, SettingType::ACTION, nullptr, {}, {}, std::move(visible)};
|
||||
}
|
||||
|
||||
static SettingInfo Value(const char* name, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange) {
|
||||
return {name, SettingType::VALUE, ptr, {}, valueRange};
|
||||
static SettingInfo Value(const char* name, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange,
|
||||
std::function<bool()> visible = nullptr) {
|
||||
return {name, SettingType::VALUE, ptr, {}, valueRange, std::move(visible)};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,12 +56,17 @@ class CategorySettingsActivity final : public ActivityWithSubactivity {
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
bool updateRequired = false;
|
||||
int selectedSettingIndex = 0;
|
||||
int selectedSettingIndex = 0; // Index into visible settings
|
||||
const char* categoryName;
|
||||
const SettingInfo* settingsList;
|
||||
int settingsCount;
|
||||
const std::function<void()> onGoBack;
|
||||
|
||||
// Helper methods for visible settings navigation
|
||||
int getVisibleSettingsCount() const;
|
||||
int getActualIndex(int visibleIndex) const; // Convert visible index to actual index
|
||||
const SettingInfo& getVisibleSetting(int visibleIndex) const;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void render() const;
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
#include "KOReaderAuthActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#include "KOReaderCredentialStore.h"
|
||||
#include "KOReaderSyncClient.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "activities/network/WifiSelectionActivity.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
void KOReaderAuthActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<KOReaderAuthActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) {
|
||||
exitActivity();
|
||||
|
||||
if (!success) {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
state = FAILED;
|
||||
errorMessage = "WiFi connection failed";
|
||||
xSemaphoreGive(renderingMutex);
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
state = AUTHENTICATING;
|
||||
statusMessage = "Authenticating...";
|
||||
xSemaphoreGive(renderingMutex);
|
||||
updateRequired = true;
|
||||
|
||||
performAuthentication();
|
||||
}
|
||||
|
||||
void KOReaderAuthActivity::performAuthentication() {
|
||||
const auto result = KOReaderSyncClient::authenticate();
|
||||
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (result == KOReaderSyncClient::OK) {
|
||||
state = SUCCESS;
|
||||
statusMessage = "Successfully authenticated!";
|
||||
} else {
|
||||
state = FAILED;
|
||||
errorMessage = KOReaderSyncClient::errorString(result);
|
||||
}
|
||||
xSemaphoreGive(renderingMutex);
|
||||
updateRequired = true;
|
||||
}
|
||||
|
||||
void KOReaderAuthActivity::onEnter() {
|
||||
ActivityWithSubactivity::onEnter();
|
||||
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
|
||||
xTaskCreate(&KOReaderAuthActivity::taskTrampoline, "KOAuthTask",
|
||||
4096, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&displayTaskHandle // Task handle
|
||||
);
|
||||
|
||||
// Turn on WiFi
|
||||
WiFi.mode(WIFI_STA);
|
||||
|
||||
// Check if already connected
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
state = AUTHENTICATING;
|
||||
statusMessage = "Authenticating...";
|
||||
updateRequired = true;
|
||||
|
||||
// Perform authentication in a separate task
|
||||
xTaskCreate(
|
||||
[](void* param) {
|
||||
auto* self = static_cast<KOReaderAuthActivity*>(param);
|
||||
self->performAuthentication();
|
||||
vTaskDelete(nullptr);
|
||||
},
|
||||
"AuthTask", 4096, this, 1, nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
// Launch WiFi selection
|
||||
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
||||
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
||||
}
|
||||
|
||||
void KOReaderAuthActivity::onExit() {
|
||||
ActivityWithSubactivity::onExit();
|
||||
|
||||
// Turn off wifi
|
||||
WiFi.disconnect(false);
|
||||
delay(100);
|
||||
WiFi.mode(WIFI_OFF);
|
||||
delay(100);
|
||||
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
vTaskDelete(displayTaskHandle);
|
||||
displayTaskHandle = nullptr;
|
||||
}
|
||||
vSemaphoreDelete(renderingMutex);
|
||||
renderingMutex = nullptr;
|
||||
}
|
||||
|
||||
void KOReaderAuthActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired && !subActivity) {
|
||||
updateRequired = false;
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
render();
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void KOReaderAuthActivity::render() {
|
||||
if (subActivity) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderer.clearScreen();
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, "KOReader Auth", true, EpdFontFamily::BOLD);
|
||||
|
||||
if (state == AUTHENTICATING) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 300, statusMessage.c_str(), true, EpdFontFamily::BOLD);
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == SUCCESS) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 280, "Success!", true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 320, "KOReader sync is ready to use");
|
||||
|
||||
const auto labels = mappedInput.mapLabels("Done", "", "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == FAILED) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 280, "Authentication Failed", true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 320, errorMessage.c_str());
|
||||
|
||||
const auto labels = mappedInput.mapLabels("Back", "", "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void KOReaderAuthActivity::loop() {
|
||||
if (subActivity) {
|
||||
subActivity->loop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == SUCCESS || state == FAILED) {
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||
onComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
#pragma once
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "activities/ActivityWithSubactivity.h"
|
||||
|
||||
/**
|
||||
* Activity for testing KOReader credentials.
|
||||
* Connects to WiFi and authenticates with the KOReader sync server.
|
||||
*/
|
||||
class KOReaderAuthActivity final : public ActivityWithSubactivity {
|
||||
public:
|
||||
explicit KOReaderAuthActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void()>& onComplete)
|
||||
: ActivityWithSubactivity("KOReaderAuth", renderer, mappedInput), onComplete(onComplete) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
bool preventAutoSleep() override { return state == CONNECTING || state == AUTHENTICATING; }
|
||||
|
||||
private:
|
||||
enum State { WIFI_SELECTION, CONNECTING, AUTHENTICATING, SUCCESS, FAILED };
|
||||
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
bool updateRequired = false;
|
||||
|
||||
State state = WIFI_SELECTION;
|
||||
std::string statusMessage;
|
||||
std::string errorMessage;
|
||||
|
||||
const std::function<void()> onComplete;
|
||||
|
||||
void onWifiSelectionComplete(bool success);
|
||||
void performAuthentication();
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void render();
|
||||
};
|
||||
@@ -1,213 +0,0 @@
|
||||
#include "KOReaderSettingsActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "KOReaderAuthActivity.h"
|
||||
#include "KOReaderCredentialStore.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "activities/util/KeyboardEntryActivity.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
namespace {
|
||||
constexpr int MENU_ITEMS = 5;
|
||||
const char* menuNames[MENU_ITEMS] = {"Username", "Password", "Sync Server URL", "Document Matching", "Authenticate"};
|
||||
} // namespace
|
||||
|
||||
void KOReaderSettingsActivity::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<KOReaderSettingsActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void KOReaderSettingsActivity::onEnter() {
|
||||
ActivityWithSubactivity::onEnter();
|
||||
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
selectedIndex = 0;
|
||||
updateRequired = true;
|
||||
|
||||
xTaskCreate(&KOReaderSettingsActivity::taskTrampoline, "KOReaderSettingsTask",
|
||||
4096, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&displayTaskHandle // Task handle
|
||||
);
|
||||
}
|
||||
|
||||
void KOReaderSettingsActivity::onExit() {
|
||||
ActivityWithSubactivity::onExit();
|
||||
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
if (displayTaskHandle) {
|
||||
vTaskDelete(displayTaskHandle);
|
||||
displayTaskHandle = nullptr;
|
||||
}
|
||||
vSemaphoreDelete(renderingMutex);
|
||||
renderingMutex = nullptr;
|
||||
}
|
||||
|
||||
void KOReaderSettingsActivity::loop() {
|
||||
if (subActivity) {
|
||||
subActivity->loop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
onBack();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||
handleSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
||||
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS;
|
||||
updateRequired = true;
|
||||
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
|
||||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
||||
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
|
||||
void KOReaderSettingsActivity::handleSelection() {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
|
||||
if (selectedIndex == 0) {
|
||||
// Username
|
||||
exitActivity();
|
||||
enterNewActivity(new KeyboardEntryActivity(
|
||||
renderer, mappedInput, "KOReader Username", KOREADER_STORE.getUsername(), 10,
|
||||
64, // maxLength
|
||||
false, // not password
|
||||
[this](const std::string& username) {
|
||||
KOREADER_STORE.setCredentials(username, KOREADER_STORE.getPassword());
|
||||
KOREADER_STORE.saveToFile();
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
},
|
||||
[this]() {
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
}));
|
||||
} else if (selectedIndex == 1) {
|
||||
// Password
|
||||
exitActivity();
|
||||
enterNewActivity(new KeyboardEntryActivity(
|
||||
renderer, mappedInput, "KOReader Password", KOREADER_STORE.getPassword(), 10,
|
||||
64, // maxLength
|
||||
false, // show characters
|
||||
[this](const std::string& password) {
|
||||
KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), password);
|
||||
KOREADER_STORE.saveToFile();
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
},
|
||||
[this]() {
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
}));
|
||||
} else if (selectedIndex == 2) {
|
||||
// Sync Server URL - prefill with https:// if empty to save typing
|
||||
const std::string currentUrl = KOREADER_STORE.getServerUrl();
|
||||
const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl;
|
||||
exitActivity();
|
||||
enterNewActivity(new KeyboardEntryActivity(
|
||||
renderer, mappedInput, "Sync Server URL", prefillUrl, 10,
|
||||
128, // maxLength - URLs can be long
|
||||
false, // not password
|
||||
[this](const std::string& url) {
|
||||
// Clear if user just left the prefilled https://
|
||||
const std::string urlToSave = (url == "https://" || url == "http://") ? "" : url;
|
||||
KOREADER_STORE.setServerUrl(urlToSave);
|
||||
KOREADER_STORE.saveToFile();
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
},
|
||||
[this]() {
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
}));
|
||||
} 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
|
||||
if (!KOREADER_STORE.hasCredentials()) {
|
||||
// Can't authenticate without credentials - just show message briefly
|
||||
xSemaphoreGive(renderingMutex);
|
||||
return;
|
||||
}
|
||||
exitActivity();
|
||||
enterNewActivity(new KOReaderAuthActivity(renderer, mappedInput, [this] {
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
}));
|
||||
}
|
||||
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
|
||||
void KOReaderSettingsActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired && !subActivity) {
|
||||
updateRequired = false;
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
render();
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void KOReaderSettingsActivity::render() {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
|
||||
// Draw header
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, "KOReader Sync", true, EpdFontFamily::BOLD);
|
||||
|
||||
// Draw selection highlight
|
||||
renderer.fillRect(0, 60 + selectedIndex * 30 - 2, pageWidth - 1, 30);
|
||||
|
||||
// Draw menu items
|
||||
for (int i = 0; i < MENU_ITEMS; i++) {
|
||||
const int settingY = 60 + i * 30;
|
||||
const bool isSelected = (i == selectedIndex);
|
||||
|
||||
renderer.drawText(UI_10_FONT_ID, 20, settingY, menuNames[i], !isSelected);
|
||||
|
||||
// Draw status for each item
|
||||
const char* status = "";
|
||||
if (i == 0) {
|
||||
status = KOREADER_STORE.getUsername().empty() ? "[Not Set]" : "[Set]";
|
||||
} else if (i == 1) {
|
||||
status = KOREADER_STORE.getPassword().empty() ? "[Not Set]" : "[Set]";
|
||||
} else if (i == 2) {
|
||||
status = KOREADER_STORE.getServerUrl().empty() ? "[Not Set]" : "[Set]";
|
||||
} else if (i == 3) {
|
||||
status = KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? "[Filename]" : "[Binary]";
|
||||
} else if (i == 4) {
|
||||
status = KOREADER_STORE.hasCredentials() ? "" : "[Set credentials first]";
|
||||
}
|
||||
|
||||
const auto width = renderer.getTextWidth(UI_10_FONT_ID, status);
|
||||
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status, !isSelected);
|
||||
}
|
||||
|
||||
// Draw button hints
|
||||
const auto labels = mappedInput.mapLabels("« Back", "Select", "", "");
|
||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
#pragma once
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "activities/ActivityWithSubactivity.h"
|
||||
|
||||
/**
|
||||
* Submenu for KOReader Sync settings.
|
||||
* Shows username, password, and authenticate options.
|
||||
*/
|
||||
class KOReaderSettingsActivity final : public ActivityWithSubactivity {
|
||||
public:
|
||||
explicit KOReaderSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void()>& onBack)
|
||||
: ActivityWithSubactivity("KOReaderSettings", renderer, mappedInput), onBack(onBack) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
|
||||
private:
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
bool updateRequired = false;
|
||||
|
||||
int selectedIndex = 0;
|
||||
const std::function<void()> onBack;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void render();
|
||||
void handleSelection();
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <builtinFonts/custom/customFonts.h>
|
||||
|
||||
#include "CategorySettingsActivity.h"
|
||||
#include "CrossPointSettings.h"
|
||||
@@ -21,9 +22,38 @@ const SettingInfo displaySettings[displaySettingsCount] = {
|
||||
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
|
||||
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})};
|
||||
|
||||
constexpr int readerSettingsCount = 9;
|
||||
// Helper to get custom font names as a vector
|
||||
std::vector<std::string> getCustomFontNamesVector() {
|
||||
std::vector<std::string> names;
|
||||
#if CUSTOM_FONT_COUNT > 0
|
||||
for (int i = 0; i < CUSTOM_FONT_COUNT; i++) {
|
||||
names.push_back(CUSTOM_FONT_NAMES[i]);
|
||||
}
|
||||
#else
|
||||
names.push_back("(none)"); // Placeholder when no custom fonts
|
||||
#endif
|
||||
return names;
|
||||
}
|
||||
|
||||
// Visibility condition for custom font settings
|
||||
bool isCustomFontSelected() { return SETTINGS.fontFamily == CrossPointSettings::CUSTOM_FONT; }
|
||||
|
||||
// Font family options - includes "Custom" only if custom fonts are available
|
||||
std::vector<std::string> getFontFamilyOptions() {
|
||||
std::vector<std::string> options = {"Bookerly", "Noto Sans"};
|
||||
#if CUSTOM_FONT_COUNT > 0
|
||||
options.push_back("Custom");
|
||||
#endif
|
||||
return options;
|
||||
}
|
||||
|
||||
constexpr int readerSettingsCount = 11;
|
||||
const SettingInfo readerSettings[readerSettingsCount] = {
|
||||
SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}),
|
||||
SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, getFontFamilyOptions()),
|
||||
SettingInfo::Enum("Custom Font", &CrossPointSettings::customFontIndex, getCustomFontNamesVector(),
|
||||
isCustomFontSelected),
|
||||
SettingInfo::Enum("Fallback Font", &CrossPointSettings::fallbackFontFamily,
|
||||
{"Bookerly", "Noto Sans"}, isCustomFontSelected),
|
||||
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}),
|
||||
SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}),
|
||||
SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}),
|
||||
@@ -45,11 +75,11 @@ const SettingInfo controlsSettings[controlsSettingsCount] = {
|
||||
SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn,
|
||||
{"Ignore", "Sleep", "Page Turn", "Dictionary"})};
|
||||
|
||||
constexpr int systemSettingsCount = 5;
|
||||
constexpr int systemSettingsCount = 4;
|
||||
const SettingInfo systemSettings[systemSettingsCount] = {
|
||||
SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout,
|
||||
{"1 min", "5 min", "10 min", "15 min", "30 min"}),
|
||||
SettingInfo::Action("KOReader Sync"), SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Clear Cache"),
|
||||
SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Clear Cache"),
|
||||
SettingInfo::Action("Check for updates")};
|
||||
} // namespace
|
||||
|
||||
|
||||
Reference in New Issue
Block a user