mod: Phase 1 - bring forward mod-exclusive files with ActivityManager migration
Brings ~55 mod-exclusive files to the upstream-based mod/master-resync branch: Activities (migrated to new ActivityManager pattern): - Clock/Time: SetTimeActivity, SetTimezoneOffsetActivity, NtpSyncActivity - Dictionary: DictionaryDefinitionActivity, DictionarySuggestionsActivity, DictionaryWordSelectActivity, LookedUpWordsActivity - Bookmark: EpubReaderBookmarkSelectionActivity - Book management: BookManageMenuActivity, EndOfBookMenuActivity - OPDS: OpdsServerListActivity, OpdsSettingsActivity - Utility: DirectoryPickerActivity, NumericStepperActivity Utilities (unchanged): - BookManager, BookSettings, BookmarkStore, BootNtpSync - Dictionary, LookupHistory, TimeSync, OpdsServerStore Libraries: PlaceholderCover, TableData, ChapterXPathIndexer Scripts: inject_mod_version, generate_book_icon, preview_placeholder_cover Docs: KOReader sync XPath mapping Migration changes: - ActivityWithSubactivity -> Activity base class - Callback constructors -> finish()/setResult() pattern - enterNewActivity() -> startActivityForResult() - Activity::RenderLock&& -> RenderLock&& These files won't compile yet - they reference mod settings and I18n strings that will be added in subsequent phases. Made-with: Cursor
This commit is contained in:
146
src/activities/settings/NtpSyncActivity.cpp
Normal file
146
src/activities/settings/NtpSyncActivity.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
#include "NtpSyncActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
#include <Logging.h>
|
||||
#include <WiFi.h>
|
||||
|
||||
#include "ActivityResult.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "activities/network/WifiSelectionActivity.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "util/BootNtpSync.h"
|
||||
#include "util/TimeSync.h"
|
||||
|
||||
static constexpr unsigned long AUTO_DISMISS_MS = 5000;
|
||||
|
||||
void NtpSyncActivity::onWifiSelectionComplete(const bool success) {
|
||||
if (!success) {
|
||||
LOG_ERR("NTP", "WiFi connection failed, exiting");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DBG("NTP", "WiFi connected, starting NTP sync");
|
||||
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
state = SYNCING;
|
||||
}
|
||||
requestUpdateAndWait();
|
||||
|
||||
const bool synced = TimeSync::waitForNtpSync(8000);
|
||||
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
state = synced ? SUCCESS : FAILED;
|
||||
if (synced) {
|
||||
successTimestamp = millis();
|
||||
}
|
||||
}
|
||||
requestUpdate();
|
||||
|
||||
if (synced) {
|
||||
LOG_DBG("NTP", "Time synced successfully");
|
||||
} else {
|
||||
LOG_ERR("NTP", "NTP sync timed out");
|
||||
}
|
||||
}
|
||||
|
||||
void NtpSyncActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
BootNtpSync::cancel();
|
||||
LOG_DBG("NTP", "Turning on WiFi...");
|
||||
WiFi.mode(WIFI_STA);
|
||||
|
||||
LOG_DBG("NTP", "Launching WifiSelectionActivity...");
|
||||
startActivityForResult(
|
||||
std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
|
||||
[this](const ActivityResult& result) {
|
||||
const bool success = !result.isCancelled && std::holds_alternative<WifiResult>(result.data);
|
||||
onWifiSelectionComplete(success);
|
||||
});
|
||||
}
|
||||
|
||||
void NtpSyncActivity::onExit() {
|
||||
Activity::onExit();
|
||||
|
||||
TimeSync::stopNtpSync();
|
||||
WiFi.disconnect(false);
|
||||
delay(100);
|
||||
WiFi.mode(WIFI_OFF);
|
||||
delay(100);
|
||||
}
|
||||
|
||||
void NtpSyncActivity::render(RenderLock&&) {
|
||||
auto metrics = UITheme::getInstance().getMetrics();
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
renderer.clearScreen();
|
||||
|
||||
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_SYNC_CLOCK));
|
||||
|
||||
const auto lineHeight = renderer.getLineHeight(UI_10_FONT_ID);
|
||||
const auto centerY = (pageHeight - lineHeight) / 2;
|
||||
|
||||
if (state == SYNCING) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_SYNCING_TIME));
|
||||
} else if (state == SUCCESS) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_TIME_SYNCED), true, EpdFontFamily::BOLD);
|
||||
|
||||
time_t now = time(nullptr);
|
||||
struct tm* t = localtime(&now);
|
||||
if (t != nullptr && t->tm_year > 100) {
|
||||
char timeBuf[32];
|
||||
if (SETTINGS.clockFormat == CrossPointSettings::CLOCK_24H) {
|
||||
snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", t->tm_hour, t->tm_min);
|
||||
} else {
|
||||
int hour12 = t->tm_hour % 12;
|
||||
if (hour12 == 0) hour12 = 12;
|
||||
snprintf(timeBuf, sizeof(timeBuf), "%d:%02d %s", hour12, t->tm_min, t->tm_hour >= 12 ? "PM" : "AM");
|
||||
}
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY + lineHeight + metrics.verticalSpacing, timeBuf);
|
||||
}
|
||||
|
||||
const unsigned long elapsed = millis() - successTimestamp;
|
||||
const int remaining = static_cast<int>((AUTO_DISMISS_MS - elapsed + 999) / 1000);
|
||||
char backLabel[32];
|
||||
snprintf(backLabel, sizeof(backLabel), "%s (%d)", tr(STR_BACK), remaining > 0 ? remaining : 1);
|
||||
const auto labels = mappedInput.mapLabels(backLabel, "", "", "");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
} else if (state == FAILED) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_SYNC_FAILED_MSG), true, EpdFontFamily::BOLD);
|
||||
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
}
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
|
||||
void NtpSyncActivity::loop() {
|
||||
if (state == SUCCESS) {
|
||||
const unsigned long elapsed = millis() - successTimestamp;
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back) || elapsed >= AUTO_DISMISS_MS) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
const int currentSecond = static_cast<int>(elapsed / 1000);
|
||||
if (currentSecond != lastCountdownSecond) {
|
||||
lastCountdownSecond = currentSecond;
|
||||
requestUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == FAILED) {
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
finish();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
22
src/activities/settings/NtpSyncActivity.h
Normal file
22
src/activities/settings/NtpSyncActivity.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "activities/Activity.h"
|
||||
|
||||
class NtpSyncActivity : public Activity {
|
||||
enum State { WIFI_SELECTION, SYNCING, SUCCESS, FAILED };
|
||||
|
||||
State state = WIFI_SELECTION;
|
||||
unsigned long successTimestamp = 0;
|
||||
int lastCountdownSecond = -1;
|
||||
|
||||
void onWifiSelectionComplete(bool success);
|
||||
|
||||
public:
|
||||
explicit NtpSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
: Activity("NtpSync", renderer, mappedInput) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(RenderLock&&) override;
|
||||
bool preventAutoSleep() override { return state == SYNCING; }
|
||||
};
|
||||
118
src/activities/settings/OpdsServerListActivity.cpp
Normal file
118
src/activities/settings/OpdsServerListActivity.cpp
Normal file
@@ -0,0 +1,118 @@
|
||||
#include "OpdsServerListActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include "ActivityResult.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "OpdsServerStore.h"
|
||||
#include "OpdsSettingsActivity.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
int OpdsServerListActivity::getItemCount() const {
|
||||
int count = static_cast<int>(OPDS_STORE.getCount());
|
||||
if (!pickerMode) {
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void OpdsServerListActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
OPDS_STORE.loadFromFile();
|
||||
selectedIndex = 0;
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void OpdsServerListActivity::onExit() { Activity::onExit(); }
|
||||
|
||||
void OpdsServerListActivity::loop() {
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||
handleSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
const int itemCount = getItemCount();
|
||||
if (itemCount > 0) {
|
||||
buttonNavigator.onNext([this, itemCount] {
|
||||
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, itemCount);
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator.onPrevious([this, itemCount] {
|
||||
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, itemCount);
|
||||
requestUpdate();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void OpdsServerListActivity::handleSelection() {
|
||||
const auto serverCount = static_cast<int>(OPDS_STORE.getCount());
|
||||
|
||||
if (pickerMode) {
|
||||
if (selectedIndex < serverCount) {
|
||||
setResult(PageResult{.page = static_cast<uint32_t>(selectedIndex)});
|
||||
finish();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const int idx = selectedIndex;
|
||||
startActivityForResult(
|
||||
std::make_unique<OpdsSettingsActivity>(renderer, mappedInput, idx < serverCount ? idx : -1),
|
||||
[this](const ActivityResult&) {
|
||||
selectedIndex = 0;
|
||||
requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
void OpdsServerListActivity::render(RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_OPDS_SERVERS));
|
||||
|
||||
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
|
||||
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2;
|
||||
const int itemCount = getItemCount();
|
||||
|
||||
if (itemCount == 0) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_NO_SERVERS));
|
||||
} else {
|
||||
const auto& servers = OPDS_STORE.getServers();
|
||||
const auto serverCount = static_cast<int>(servers.size());
|
||||
|
||||
// Primary label: server name (falling back to URL if unnamed).
|
||||
// Secondary label: server URL (shown as subtitle when name is set).
|
||||
GUI.drawList(
|
||||
renderer, Rect{0, contentTop, pageWidth, contentHeight}, itemCount, selectedIndex,
|
||||
[&servers, serverCount](int index) {
|
||||
if (index < serverCount) {
|
||||
const auto& server = servers[index];
|
||||
return server.name.empty() ? server.url : server.name;
|
||||
}
|
||||
return std::string(I18n::getInstance().get(StrId::STR_ADD_SERVER));
|
||||
},
|
||||
[&servers, serverCount](int index) {
|
||||
if (index < serverCount && !servers[index].name.empty()) {
|
||||
return servers[index].url;
|
||||
}
|
||||
return std::string("");
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
31
src/activities/settings/OpdsServerListActivity.h
Normal file
31
src/activities/settings/OpdsServerListActivity.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "activities/Activity.h"
|
||||
#include "util/ButtonNavigator.h"
|
||||
|
||||
/**
|
||||
* Activity showing the list of configured OPDS servers.
|
||||
* Allows adding new servers and editing/deleting existing ones.
|
||||
* Used from Settings and also as a server picker from the home screen.
|
||||
*
|
||||
* When pickerMode is true, selecting a server returns PageResult{.page = serverIndex} and finishes.
|
||||
* When pickerMode is false (settings mode), selecting opens OpdsSettingsActivity for edit/add.
|
||||
*/
|
||||
class OpdsServerListActivity final : public Activity {
|
||||
public:
|
||||
explicit OpdsServerListActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, bool pickerMode = false)
|
||||
: Activity("OpdsServerList", renderer, mappedInput), pickerMode(pickerMode) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(RenderLock&&) override;
|
||||
|
||||
private:
|
||||
ButtonNavigator buttonNavigator;
|
||||
int selectedIndex = 0;
|
||||
bool pickerMode;
|
||||
|
||||
int getItemCount() const;
|
||||
void handleSelection();
|
||||
};
|
||||
225
src/activities/settings/OpdsSettingsActivity.cpp
Normal file
225
src/activities/settings/OpdsSettingsActivity.cpp
Normal file
@@ -0,0 +1,225 @@
|
||||
#include "OpdsSettingsActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "ActivityResult.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "OpdsServerStore.h"
|
||||
#include "activities/util/DirectoryPickerActivity.h"
|
||||
#include "activities/util/KeyboardEntryActivity.h"
|
||||
#include "activities/util/NumericStepperActivity.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
namespace {
|
||||
// Editable fields: Position, Name, URL, Username, Password, Download Path, After Download.
|
||||
// Existing servers also show a Delete option (BASE_ITEMS + 1).
|
||||
constexpr int BASE_ITEMS = 7;
|
||||
} // namespace
|
||||
|
||||
int OpdsSettingsActivity::getMenuItemCount() const {
|
||||
return isNewServer ? BASE_ITEMS : BASE_ITEMS + 1; // +1 for Delete
|
||||
}
|
||||
|
||||
void OpdsSettingsActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
selectedIndex = 0;
|
||||
isNewServer = (serverIndex < 0);
|
||||
|
||||
if (!isNewServer) {
|
||||
const auto* server = OPDS_STORE.getServer(static_cast<size_t>(serverIndex));
|
||||
if (server) {
|
||||
editServer = *server;
|
||||
} else {
|
||||
// Server was deleted between navigation and entering this screen — treat as new
|
||||
isNewServer = true;
|
||||
serverIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void OpdsSettingsActivity::onExit() { Activity::onExit(); }
|
||||
|
||||
void OpdsSettingsActivity::loop() {
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||
handleSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
const int menuItems = getMenuItemCount();
|
||||
buttonNavigator.onNext([this, menuItems] {
|
||||
selectedIndex = (selectedIndex + 1) % menuItems;
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator.onPrevious([this, menuItems] {
|
||||
selectedIndex = (selectedIndex + menuItems - 1) % menuItems;
|
||||
requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
void OpdsSettingsActivity::saveServer() {
|
||||
if (isNewServer) {
|
||||
OPDS_STORE.addServer(editServer);
|
||||
isNewServer = false;
|
||||
} else {
|
||||
OPDS_STORE.updateServer(static_cast<size_t>(serverIndex), editServer);
|
||||
}
|
||||
|
||||
// Re-locate our server after add/update may have re-sorted the vector
|
||||
const auto& servers = OPDS_STORE.getServers();
|
||||
for (size_t i = 0; i < servers.size(); i++) {
|
||||
if (servers[i].url == editServer.url && servers[i].name == editServer.name) {
|
||||
serverIndex = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OpdsSettingsActivity::handleSelection() {
|
||||
if (selectedIndex == 0) {
|
||||
// Position (sort order)
|
||||
startActivityForResult(
|
||||
std::make_unique<NumericStepperActivity>(renderer, mappedInput, tr(STR_POSITION), editServer.sortOrder, 1, 99),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled && std::holds_alternative<PageResult>(result.data)) {
|
||||
editServer.sortOrder = static_cast<int>(std::get<PageResult>(result.data).page);
|
||||
saveServer();
|
||||
}
|
||||
requestUpdate();
|
||||
});
|
||||
} else if (selectedIndex == 1) {
|
||||
// Server Name
|
||||
startActivityForResult(
|
||||
std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_SERVER_NAME), editServer.name, 63, false),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled && std::holds_alternative<KeyboardResult>(result.data)) {
|
||||
editServer.name = std::get<KeyboardResult>(result.data).text;
|
||||
saveServer();
|
||||
}
|
||||
requestUpdate();
|
||||
});
|
||||
} else if (selectedIndex == 2) {
|
||||
// Server URL
|
||||
startActivityForResult(
|
||||
std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_OPDS_SERVER_URL), editServer.url, 127,
|
||||
false),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled && std::holds_alternative<KeyboardResult>(result.data)) {
|
||||
editServer.url = std::get<KeyboardResult>(result.data).text;
|
||||
saveServer();
|
||||
}
|
||||
requestUpdate();
|
||||
});
|
||||
} else if (selectedIndex == 3) {
|
||||
// Username
|
||||
startActivityForResult(
|
||||
std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_USERNAME), editServer.username, 63,
|
||||
false),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled && std::holds_alternative<KeyboardResult>(result.data)) {
|
||||
editServer.username = std::get<KeyboardResult>(result.data).text;
|
||||
saveServer();
|
||||
}
|
||||
requestUpdate();
|
||||
});
|
||||
} else if (selectedIndex == 4) {
|
||||
// Password
|
||||
startActivityForResult(
|
||||
std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_PASSWORD), editServer.password, 63,
|
||||
false),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled && std::holds_alternative<KeyboardResult>(result.data)) {
|
||||
editServer.password = std::get<KeyboardResult>(result.data).text;
|
||||
saveServer();
|
||||
}
|
||||
requestUpdate();
|
||||
});
|
||||
} else if (selectedIndex == 5) {
|
||||
// Download Path
|
||||
startActivityForResult(
|
||||
std::make_unique<DirectoryPickerActivity>(renderer, mappedInput, editServer.downloadPath),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled && std::holds_alternative<KeyboardResult>(result.data)) {
|
||||
editServer.downloadPath = std::get<KeyboardResult>(result.data).text;
|
||||
saveServer();
|
||||
}
|
||||
requestUpdate();
|
||||
});
|
||||
} else if (selectedIndex == 6) {
|
||||
// After Download — toggle between 0 (back to listing) and 1 (open book)
|
||||
editServer.afterDownloadAction = editServer.afterDownloadAction == 0 ? 1 : 0;
|
||||
saveServer();
|
||||
requestUpdate();
|
||||
} else if (selectedIndex == 7 && !isNewServer) {
|
||||
// Delete server
|
||||
OPDS_STORE.removeServer(static_cast<size_t>(serverIndex));
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
void OpdsSettingsActivity::render(RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
const char* header = isNewServer ? tr(STR_ADD_SERVER) : tr(STR_OPDS_BROWSER);
|
||||
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, header);
|
||||
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
|
||||
tr(STR_CALIBRE_URL_HINT));
|
||||
|
||||
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + metrics.tabBarHeight;
|
||||
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2;
|
||||
const int menuItems = getMenuItemCount();
|
||||
|
||||
const StrId fieldNames[] = {StrId::STR_POSITION, StrId::STR_SERVER_NAME, StrId::STR_OPDS_SERVER_URL,
|
||||
StrId::STR_USERNAME, StrId::STR_PASSWORD, StrId::STR_DOWNLOAD_PATH,
|
||||
StrId::STR_AFTER_DOWNLOAD};
|
||||
|
||||
GUI.drawList(
|
||||
renderer, Rect{0, contentTop, pageWidth, contentHeight}, menuItems, static_cast<int>(selectedIndex),
|
||||
[this, &fieldNames](int index) {
|
||||
if (index < BASE_ITEMS) {
|
||||
return std::string(I18N.get(fieldNames[index]));
|
||||
}
|
||||
return std::string(tr(STR_DELETE_SERVER));
|
||||
},
|
||||
nullptr, nullptr,
|
||||
[this](int index) {
|
||||
if (index == 0) {
|
||||
return std::to_string(editServer.sortOrder);
|
||||
} else if (index == 1) {
|
||||
return editServer.name.empty() ? std::string(tr(STR_NOT_SET)) : editServer.name;
|
||||
} else if (index == 2) {
|
||||
return editServer.url.empty() ? std::string(tr(STR_NOT_SET)) : editServer.url;
|
||||
} else if (index == 3) {
|
||||
return editServer.username.empty() ? std::string(tr(STR_NOT_SET)) : editServer.username;
|
||||
} else if (index == 4) {
|
||||
return editServer.password.empty() ? std::string(tr(STR_NOT_SET)) : std::string("******");
|
||||
} else if (index == 5) {
|
||||
return editServer.downloadPath;
|
||||
} else if (index == 6) {
|
||||
return std::string(editServer.afterDownloadAction == 0 ? tr(STR_BACK_TO_LISTING) : tr(STR_OPEN_BOOK));
|
||||
}
|
||||
return std::string("");
|
||||
},
|
||||
true);
|
||||
|
||||
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();
|
||||
}
|
||||
36
src/activities/settings/OpdsSettingsActivity.h
Normal file
36
src/activities/settings/OpdsSettingsActivity.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include "OpdsServerStore.h"
|
||||
#include "activities/Activity.h"
|
||||
#include "util/ButtonNavigator.h"
|
||||
|
||||
/**
|
||||
* Edit screen for a single OPDS server.
|
||||
* Shows Name, URL, Username, Password fields and a Delete option.
|
||||
* Used for both adding new servers and editing existing ones.
|
||||
*/
|
||||
class OpdsSettingsActivity final : public Activity {
|
||||
public:
|
||||
/**
|
||||
* @param serverIndex Index into OpdsServerStore, or -1 for a new server
|
||||
*/
|
||||
explicit OpdsSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, int serverIndex = -1)
|
||||
: Activity("OpdsSettings", renderer, mappedInput), serverIndex(serverIndex) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(RenderLock&&) override;
|
||||
|
||||
private:
|
||||
ButtonNavigator buttonNavigator;
|
||||
|
||||
size_t selectedIndex = 0;
|
||||
int serverIndex;
|
||||
OpdsServer editServer;
|
||||
bool isNewServer = false;
|
||||
|
||||
int getMenuItemCount() const;
|
||||
void handleSelection();
|
||||
void saveServer();
|
||||
};
|
||||
157
src/activities/settings/SetTimeActivity.cpp
Normal file
157
src/activities/settings/SetTimeActivity.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
#include "SetTimeActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
#include <sys/time.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
void SetTimeActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
// Initialize from current system time if it's been set (year > 2000)
|
||||
time_t now = time(nullptr);
|
||||
struct tm* t = localtime(&now);
|
||||
if (t != nullptr && t->tm_year > 100) {
|
||||
hour = t->tm_hour;
|
||||
minute = t->tm_min;
|
||||
} else {
|
||||
hour = 12;
|
||||
minute = 0;
|
||||
}
|
||||
|
||||
selectedField = 0;
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void SetTimeActivity::onExit() { Activity::onExit(); }
|
||||
|
||||
void SetTimeActivity::loop() {
|
||||
// Back button: discard and exit
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm button: apply time and exit
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||
applyTime();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Left/Right: switch between hour and minute fields
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Left)) {
|
||||
selectedField = 0;
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Right)) {
|
||||
selectedField = 1;
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
// Up/Down: increment/decrement the selected field
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up)) {
|
||||
if (selectedField == 0) {
|
||||
hour = (hour + 1) % 24;
|
||||
} else {
|
||||
minute = (minute + 1) % 60;
|
||||
}
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Down)) {
|
||||
if (selectedField == 0) {
|
||||
hour = (hour + 23) % 24;
|
||||
} else {
|
||||
minute = (minute + 59) % 60;
|
||||
}
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void SetTimeActivity::render(RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const int lineHeight12 = renderer.getLineHeight(UI_12_FONT_ID);
|
||||
|
||||
// Title
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 20, tr(STR_SET_TIME), true, EpdFontFamily::BOLD);
|
||||
|
||||
// Format hour and minute strings
|
||||
char hourStr[4];
|
||||
char minuteStr[4];
|
||||
snprintf(hourStr, sizeof(hourStr), "%02d", hour);
|
||||
snprintf(minuteStr, sizeof(minuteStr), "%02d", minute);
|
||||
|
||||
const int colonWidth = renderer.getTextWidth(UI_12_FONT_ID, " : ");
|
||||
const int digitWidth = renderer.getTextWidth(UI_12_FONT_ID, "00");
|
||||
const int totalWidth = digitWidth * 2 + colonWidth;
|
||||
const int startX = (pageWidth - totalWidth) / 2;
|
||||
const int timeY = 80;
|
||||
|
||||
// Draw selection highlight behind the selected field
|
||||
constexpr int highlightPad = 6;
|
||||
if (selectedField == 0) {
|
||||
renderer.fillRoundedRect(startX - highlightPad, timeY - 4, digitWidth + highlightPad * 2, lineHeight12 + 8, 6,
|
||||
Color::LightGray);
|
||||
} else {
|
||||
renderer.fillRoundedRect(startX + digitWidth + colonWidth - highlightPad, timeY - 4, digitWidth + highlightPad * 2,
|
||||
lineHeight12 + 8, 6, Color::LightGray);
|
||||
}
|
||||
|
||||
// Draw the time digits and colon
|
||||
renderer.drawText(UI_12_FONT_ID, startX, timeY, hourStr, true);
|
||||
renderer.drawText(UI_12_FONT_ID, startX + digitWidth, timeY, " : ", true);
|
||||
renderer.drawText(UI_12_FONT_ID, startX + digitWidth + colonWidth, timeY, minuteStr, true);
|
||||
|
||||
// Draw up/down arrows above and below the selected field
|
||||
const int arrowX = (selectedField == 0) ? startX + digitWidth / 2 : startX + digitWidth + colonWidth + digitWidth / 2;
|
||||
const int arrowUpY = timeY - 20;
|
||||
const int arrowDownY = timeY + lineHeight12 + 12;
|
||||
// Up arrow (simple triangle using lines)
|
||||
constexpr int arrowSize = 6;
|
||||
for (int row = 0; row < arrowSize; row++) {
|
||||
renderer.drawLine(arrowX - row, arrowUpY + row, arrowX + row, arrowUpY + row);
|
||||
}
|
||||
// Down arrow
|
||||
for (int row = 0; row < arrowSize; row++) {
|
||||
renderer.drawLine(arrowX - row, arrowDownY + arrowSize - 1 - row, arrowX + row, arrowDownY + arrowSize - 1 - row);
|
||||
}
|
||||
|
||||
// Button hints
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SAVE), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
|
||||
void SetTimeActivity::applyTime() {
|
||||
time_t now = time(nullptr);
|
||||
struct tm newTime = {};
|
||||
struct tm* current = localtime(&now);
|
||||
if (current != nullptr && current->tm_year > 100) {
|
||||
newTime = *current;
|
||||
} else {
|
||||
// If time was never set, use a reasonable date (2025-01-01)
|
||||
newTime.tm_year = 125; // years since 1900
|
||||
newTime.tm_mon = 0;
|
||||
newTime.tm_mday = 1;
|
||||
}
|
||||
newTime.tm_hour = hour;
|
||||
newTime.tm_min = minute;
|
||||
newTime.tm_sec = 0;
|
||||
time_t newEpoch = mktime(&newTime);
|
||||
struct timeval tv = {.tv_sec = newEpoch, .tv_usec = 0};
|
||||
settimeofday(&tv, nullptr);
|
||||
}
|
||||
23
src/activities/settings/SetTimeActivity.h
Normal file
23
src/activities/settings/SetTimeActivity.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "activities/Activity.h"
|
||||
|
||||
class SetTimeActivity final : public Activity {
|
||||
public:
|
||||
explicit SetTimeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
: Activity("SetTime", renderer, mappedInput) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(RenderLock&&) override;
|
||||
|
||||
private:
|
||||
|
||||
// 0 = editing hours, 1 = editing minutes
|
||||
uint8_t selectedField = 0;
|
||||
int hour = 12;
|
||||
int minute = 0;
|
||||
|
||||
void applyTime();
|
||||
};
|
||||
101
src/activities/settings/SetTimezoneOffsetActivity.cpp
Normal file
101
src/activities/settings/SetTimezoneOffsetActivity.cpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#include "SetTimezoneOffsetActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
void SetTimezoneOffsetActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
offsetHours = SETTINGS.timezoneOffsetHours;
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void SetTimezoneOffsetActivity::onExit() { Activity::onExit(); }
|
||||
|
||||
void SetTimezoneOffsetActivity::loop() {
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||
SETTINGS.timezoneOffsetHours = offsetHours;
|
||||
SETTINGS.saveToFile();
|
||||
// Apply timezone immediately
|
||||
setenv("TZ", SETTINGS.getTimezonePosixStr(), 1);
|
||||
tzset();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Up)) {
|
||||
if (offsetHours < 14) {
|
||||
offsetHours++;
|
||||
requestUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Down)) {
|
||||
if (offsetHours > -12) {
|
||||
offsetHours--;
|
||||
requestUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void SetTimezoneOffsetActivity::render(RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const int lineHeight12 = renderer.getLineHeight(UI_12_FONT_ID);
|
||||
|
||||
// Title
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, 20, tr(STR_SET_UTC_OFFSET), true, EpdFontFamily::BOLD);
|
||||
|
||||
// Format the offset string
|
||||
char offsetStr[16];
|
||||
if (offsetHours >= 0) {
|
||||
snprintf(offsetStr, sizeof(offsetStr), "UTC+%d", offsetHours);
|
||||
} else {
|
||||
snprintf(offsetStr, sizeof(offsetStr), "UTC%d", offsetHours);
|
||||
}
|
||||
|
||||
const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, offsetStr);
|
||||
const int startX = (pageWidth - textWidth) / 2;
|
||||
const int valueY = 80;
|
||||
|
||||
// Draw selection highlight
|
||||
constexpr int highlightPad = 10;
|
||||
renderer.fillRoundedRect(startX - highlightPad, valueY - 4, textWidth + highlightPad * 2, lineHeight12 + 8, 6,
|
||||
Color::LightGray);
|
||||
|
||||
// Draw the offset text
|
||||
renderer.drawText(UI_12_FONT_ID, startX, valueY, offsetStr, true);
|
||||
|
||||
// Draw up/down arrows
|
||||
const int arrowX = pageWidth / 2;
|
||||
const int arrowUpY = valueY - 20;
|
||||
const int arrowDownY = valueY + lineHeight12 + 12;
|
||||
constexpr int arrowSize = 6;
|
||||
for (int row = 0; row < arrowSize; row++) {
|
||||
renderer.drawLine(arrowX - row, arrowUpY + row, arrowX + row, arrowUpY + row);
|
||||
}
|
||||
for (int row = 0; row < arrowSize; row++) {
|
||||
renderer.drawLine(arrowX - row, arrowDownY + arrowSize - 1 - row, arrowX + row, arrowDownY + arrowSize - 1 - row);
|
||||
}
|
||||
|
||||
// Button hints
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SAVE), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
17
src/activities/settings/SetTimezoneOffsetActivity.h
Normal file
17
src/activities/settings/SetTimezoneOffsetActivity.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "activities/Activity.h"
|
||||
|
||||
class SetTimezoneOffsetActivity final : public Activity {
|
||||
public:
|
||||
explicit SetTimezoneOffsetActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
: Activity("SetTZOffset", renderer, mappedInput) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(RenderLock&&) override;
|
||||
|
||||
private:
|
||||
int8_t offsetHours = 0;
|
||||
};
|
||||
Reference in New Issue
Block a user