feat: Support for multiple OPDS servers (#1209)
## Summary * Add support for configuring and using multiple OPDS servers, replacing the previous single-server limitation. Closes https://github.com/crosspoint-reader/crosspoint-reader/issues/1178 * New OpdsServerStore singleton (modeled after WifiCredentialStore) that persists up to 8 OPDS servers to /.crosspoint/opds.json with MAC-based password obfuscation. * One-time migration from legacy single-server fields in CrossPointSettings to the new store on first boot. * New OpdsServerListActivity for the device UI — works in two modes: a settings list (add/edit/delete servers) and a picker (select which server to browse). When only one server is configured, the picker is skipped automatically. * Renamed CalibreSettingsActivity → OpdsSettingsActivity for clarity. It now edits individual OpdsServer entries (name, URL, username, password, delete). * OpdsBookBrowserActivity now receives an OpdsServer at construction and uses its credentials for all fetches/downloads, and shows the server name in the header. * HttpDownloader::fetchUrl and downloadToFile accept optional per-call username/password parameters instead of reading from global settings. * REST API endpoints on CrossPointWebServer: GET /api/opds, POST /api/opds, POST /api/opds/delete — passwords are never exposed over the API (only a hasPassword flag), and omitting the password field on update preserves the existing one. * Web UI (SettingsPage.html) with dynamic OPDS server management cards — add, edit, save, and delete servers from the browser. <img width="932" height="906" alt="SCR-20260416-stvu" src="https://github.com/user-attachments/assets/a8f18d84-4204-46a0-bb31-b73d24b3255f" /> ## Additional Context * The OpdsServerStore JSON format and obfuscation scheme are identical to WifiCredentialStore, so the same JsonSettingsIO infrastructure handles both. * The web API uses POST /api/opds/delete instead of DELETE /api/opds because the ESP32 WebServer doesn't support the DELETE method with a request body. * Existing single-server configurations are migrated automatically — no user action required. After migration the legacy CrossPointSettings fields are cleared so it only runs once. * The HttpDownloader changes are backward-compatible: the credential parameters default to empty strings, so existing callers are unaffected. --- ### 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? _**< YES >**_ --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
parent
c5f82709c0
commit
1cf2239742
@@ -1,131 +0,0 @@
|
||||
#include "CalibreSettingsActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "activities/util/KeyboardEntryActivity.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
namespace {
|
||||
constexpr int MENU_ITEMS = 3;
|
||||
const StrId menuNames[MENU_ITEMS] = {StrId::STR_CALIBRE_WEB_URL, StrId::STR_USERNAME, StrId::STR_PASSWORD};
|
||||
} // namespace
|
||||
|
||||
void CalibreSettingsActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
selectedIndex = 0;
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void CalibreSettingsActivity::onExit() { Activity::onExit(); }
|
||||
|
||||
void CalibreSettingsActivity::loop() {
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||
handleSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle navigation
|
||||
buttonNavigator.onNext([this] {
|
||||
selectedIndex = (selectedIndex + 1) % MENU_ITEMS;
|
||||
requestUpdate();
|
||||
});
|
||||
|
||||
buttonNavigator.onPrevious([this] {
|
||||
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS;
|
||||
requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
void CalibreSettingsActivity::handleSelection() {
|
||||
if (selectedIndex == 0) {
|
||||
// OPDS Server URL - prefill with https:// if empty to save typing
|
||||
const std::string currentUrl = SETTINGS.opdsServerUrl;
|
||||
const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl;
|
||||
startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_CALIBRE_WEB_URL),
|
||||
prefillUrl, 127, InputType::Url),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
const auto& kb = std::get<KeyboardResult>(result.data);
|
||||
const std::string urlToSave =
|
||||
(kb.text == "https://" || kb.text == "http://") ? "" : kb.text;
|
||||
strncpy(SETTINGS.opdsServerUrl, urlToSave.c_str(), sizeof(SETTINGS.opdsServerUrl) - 1);
|
||||
SETTINGS.opdsServerUrl[sizeof(SETTINGS.opdsServerUrl) - 1] = '\0';
|
||||
SETTINGS.saveToFile();
|
||||
}
|
||||
});
|
||||
} else if (selectedIndex == 1) {
|
||||
// Username
|
||||
startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_USERNAME),
|
||||
SETTINGS.opdsUsername, 63, InputType::Text),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
const auto& kb = std::get<KeyboardResult>(result.data);
|
||||
strncpy(SETTINGS.opdsUsername, kb.text.c_str(), sizeof(SETTINGS.opdsUsername) - 1);
|
||||
SETTINGS.opdsUsername[sizeof(SETTINGS.opdsUsername) - 1] = '\0';
|
||||
SETTINGS.saveToFile();
|
||||
}
|
||||
});
|
||||
} else if (selectedIndex == 2) {
|
||||
// Password
|
||||
startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_PASSWORD),
|
||||
SETTINGS.opdsPassword, 63, InputType::Password),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
const auto& kb = std::get<KeyboardResult>(result.data);
|
||||
strncpy(SETTINGS.opdsPassword, kb.text.c_str(), sizeof(SETTINGS.opdsPassword) - 1);
|
||||
SETTINGS.opdsPassword[sizeof(SETTINGS.opdsPassword) - 1] = '\0';
|
||||
SETTINGS.saveToFile();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void CalibreSettingsActivity::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_BROWSER));
|
||||
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;
|
||||
GUI.drawList(
|
||||
renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast<int>(MENU_ITEMS),
|
||||
static_cast<int>(selectedIndex), [](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr,
|
||||
nullptr,
|
||||
[this](int index) {
|
||||
// Draw status for each setting
|
||||
if (index == 0) {
|
||||
return (strlen(SETTINGS.opdsServerUrl) > 0) ? std::string(SETTINGS.opdsServerUrl)
|
||||
: std::string(tr(STR_NOT_SET));
|
||||
} else if (index == 1) {
|
||||
return (strlen(SETTINGS.opdsUsername) > 0) ? std::string(SETTINGS.opdsUsername)
|
||||
: std::string(tr(STR_NOT_SET));
|
||||
} else if (index == 2) {
|
||||
return (strlen(SETTINGS.opdsPassword) > 0) ? std::string("******") : std::string(tr(STR_NOT_SET));
|
||||
}
|
||||
return std::string(tr(STR_NOT_SET));
|
||||
},
|
||||
true);
|
||||
|
||||
// Draw help text at bottom
|
||||
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();
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "activities/Activity.h"
|
||||
#include "util/ButtonNavigator.h"
|
||||
|
||||
/**
|
||||
* Submenu for OPDS Browser settings.
|
||||
* Shows OPDS Server URL and HTTP authentication options.
|
||||
*/
|
||||
class CalibreSettingsActivity final : public Activity {
|
||||
public:
|
||||
explicit CalibreSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
: Activity("CalibreSettings", renderer, mappedInput) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(RenderLock&&) override;
|
||||
|
||||
private:
|
||||
ButtonNavigator buttonNavigator;
|
||||
|
||||
size_t selectedIndex = 0;
|
||||
void handleSelection();
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
#include "OpdsServerListActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "OpdsServerStore.h"
|
||||
#include "OpdsSettingsActivity.h"
|
||||
#include "activities/ActivityManager.h"
|
||||
#include "activities/browser/OpdsBookBrowserActivity.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
int OpdsServerListActivity::getItemCount() const {
|
||||
int count = static_cast<int>(OPDS_STORE.getCount());
|
||||
// In settings mode, append a virtual "Add Server" item; in picker mode, only show real servers
|
||||
if (!pickerMode) {
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void OpdsServerListActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
|
||||
// Reload from disk in case servers were added/removed by a subactivity or the web UI
|
||||
OPDS_STORE.loadFromFile();
|
||||
selectedIndex = 0;
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
void OpdsServerListActivity::onExit() { Activity::onExit(); }
|
||||
|
||||
void OpdsServerListActivity::loop() {
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
if (pickerMode) {
|
||||
activityManager.goHome();
|
||||
} else {
|
||||
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) {
|
||||
// Picker mode: selecting a server navigates to the OPDS browser
|
||||
if (selectedIndex < serverCount) {
|
||||
const auto* server = OPDS_STORE.getServer(static_cast<size_t>(selectedIndex));
|
||||
if (server) {
|
||||
activityManager.replaceActivity(std::make_unique<OpdsBookBrowserActivity>(renderer, mappedInput, *server));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Settings mode: open editor for selected server, or create a new one
|
||||
auto resultHandler = [this](const ActivityResult&) {
|
||||
// Reload server list when returning from editor
|
||||
OPDS_STORE.loadFromFile();
|
||||
selectedIndex = 0;
|
||||
};
|
||||
|
||||
if (selectedIndex < serverCount) {
|
||||
startActivityForResult(std::make_unique<OpdsSettingsActivity>(renderer, mappedInput, selectedIndex), resultHandler);
|
||||
} else {
|
||||
startActivityForResult(std::make_unique<OpdsSettingsActivity>(renderer, mappedInput, -1), resultHandler);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#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.
|
||||
* When pickerMode is true, selecting a server navigates to the OPDS browser
|
||||
* instead of opening the editor (used from the home screen).
|
||||
*/
|
||||
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 = false;
|
||||
|
||||
int getItemCount() const;
|
||||
void handleSelection();
|
||||
};
|
||||
@@ -0,0 +1,222 @@
|
||||
#include "OpdsSettingsActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "OpdsServerStore.h"
|
||||
#include "activities/util/KeyboardEntryActivity.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
namespace {
|
||||
// Editable fields: Name, URL, Username, Password.
|
||||
// Existing servers also show a Delete option (BASE_ITEMS + 1).
|
||||
constexpr int BASE_ITEMS = 4;
|
||||
} // 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);
|
||||
showSaveError = false;
|
||||
|
||||
if (!isNewServer) {
|
||||
// Edit flow: copy the selected server into local editable state.
|
||||
// Changes are persisted field-by-field through saveServer().
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
bool OpdsSettingsActivity::saveServer() {
|
||||
bool success = false;
|
||||
|
||||
if (isNewServer) {
|
||||
// Create flow: first save inserts a new server record into the multi-server store.
|
||||
success = OPDS_STORE.addServer(editServer);
|
||||
if (success) {
|
||||
// After the first successful save, promote to an existing server so
|
||||
// subsequent field edits update in-place rather than creating duplicates.
|
||||
isNewServer = false;
|
||||
serverIndex = static_cast<int>(OPDS_STORE.getCount()) - 1;
|
||||
} else {
|
||||
LOG_ERR("OPS", "Failed to add OPDS server");
|
||||
}
|
||||
} else {
|
||||
// Edit flow: update the same server entry in-place.
|
||||
success = OPDS_STORE.updateServer(static_cast<size_t>(serverIndex), editServer);
|
||||
if (!success) {
|
||||
LOG_ERR("OPS", "Failed to update OPDS server at index %d", serverIndex);
|
||||
}
|
||||
}
|
||||
|
||||
showSaveError = !success;
|
||||
if (showSaveError) {
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
void OpdsSettingsActivity::handleSelection() {
|
||||
// Each field edit is saved immediately so partially configured servers
|
||||
// survive navigation and power-loss scenarios.
|
||||
if (selectedIndex == 0) {
|
||||
// Server Name
|
||||
auto handler = [this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
const auto& kb = std::get<KeyboardResult>(result.data);
|
||||
editServer.name = kb.text;
|
||||
saveServer();
|
||||
requestUpdate();
|
||||
}
|
||||
};
|
||||
startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_SERVER_NAME),
|
||||
editServer.name, 63, InputType::Text),
|
||||
handler);
|
||||
} else if (selectedIndex == 1) {
|
||||
// Server URL
|
||||
const std::string prefillUrl = editServer.url.empty() ? "https://" : editServer.url;
|
||||
auto handler = [this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
const auto& kb = std::get<KeyboardResult>(result.data);
|
||||
editServer.url = (kb.text == "https://" || kb.text == "http://") ? "" : kb.text;
|
||||
saveServer();
|
||||
requestUpdate();
|
||||
}
|
||||
};
|
||||
startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_OPDS_SERVER_URL),
|
||||
prefillUrl, 127, InputType::Url),
|
||||
handler);
|
||||
} else if (selectedIndex == 2) {
|
||||
// Username
|
||||
auto handler = [this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
const auto& kb = std::get<KeyboardResult>(result.data);
|
||||
editServer.username = kb.text;
|
||||
saveServer();
|
||||
requestUpdate();
|
||||
}
|
||||
};
|
||||
startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_USERNAME),
|
||||
editServer.username, 63, InputType::Text),
|
||||
handler);
|
||||
} else if (selectedIndex == 3) {
|
||||
// Password
|
||||
auto handler = [this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
const auto& kb = std::get<KeyboardResult>(result.data);
|
||||
editServer.password = kb.text;
|
||||
saveServer();
|
||||
requestUpdate();
|
||||
}
|
||||
};
|
||||
startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_PASSWORD),
|
||||
editServer.password, 63, InputType::Password),
|
||||
handler);
|
||||
} else if (selectedIndex == 4 && !isNewServer) {
|
||||
// Delete flow is only available for existing servers.
|
||||
if (!OPDS_STORE.removeServer(static_cast<size_t>(serverIndex))) {
|
||||
LOG_ERR("OPS", "Failed to remove OPDS server at index %d", serverIndex);
|
||||
showSaveError = true;
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
void OpdsSettingsActivity::render(RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
// Reuse STR_OPDS_BROWSER as the "edit existing server" title.
|
||||
// New server creation uses STR_ADD_SERVER.
|
||||
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_SERVER_NAME, StrId::STR_OPDS_SERVER_URL, StrId::STR_USERNAME,
|
||||
StrId::STR_PASSWORD};
|
||||
|
||||
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 editServer.name.empty() ? std::string(tr(STR_NOT_SET)) : editServer.name;
|
||||
} else if (index == 1) {
|
||||
return editServer.url.empty() ? std::string(tr(STR_NOT_SET)) : editServer.url;
|
||||
} else if (index == 2) {
|
||||
return editServer.username.empty() ? std::string(tr(STR_NOT_SET)) : editServer.username;
|
||||
} else if (index == 3) {
|
||||
return editServer.password.empty() ? std::string(tr(STR_NOT_SET)) : std::string("******");
|
||||
}
|
||||
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);
|
||||
|
||||
if (showSaveError) {
|
||||
GUI.drawPopup(renderer, tr(STR_ERROR_GENERAL_FAILURE));
|
||||
}
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#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;
|
||||
bool showSaveError = false;
|
||||
|
||||
int getMenuItemCount() const;
|
||||
void handleSelection();
|
||||
bool saveServer();
|
||||
};
|
||||
@@ -4,12 +4,12 @@
|
||||
#include <Logging.h>
|
||||
|
||||
#include "ButtonRemapActivity.h"
|
||||
#include "CalibreSettingsActivity.h"
|
||||
#include "ClearCacheActivity.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "KOReaderSettingsActivity.h"
|
||||
#include "LanguageSelectActivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "OpdsServerListActivity.h"
|
||||
#include "OtaUpdateActivity.h"
|
||||
#include "SettingsList.h"
|
||||
#include "StatusBarSettingsActivity.h"
|
||||
@@ -48,7 +48,7 @@ void SettingsActivity::onEnter() {
|
||||
SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_BROWSER, SettingAction::OPDSBrowser));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_SERVERS, SettingAction::OPDSBrowser));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_CLEAR_READING_CACHE, SettingAction::ClearCache));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates));
|
||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_LANGUAGE, SettingAction::Language));
|
||||
@@ -178,7 +178,7 @@ void SettingsActivity::toggleCurrentSetting() {
|
||||
startActivityForResult(std::make_unique<KOReaderSettingsActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
case SettingAction::OPDSBrowser:
|
||||
startActivityForResult(std::make_unique<CalibreSettingsActivity>(renderer, mappedInput), resultHandler);
|
||||
startActivityForResult(std::make_unique<OpdsServerListActivity>(renderer, mappedInput), resultHandler);
|
||||
break;
|
||||
case SettingAction::Network:
|
||||
startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput, false), resultHandler);
|
||||
|
||||
Reference in New Issue
Block a user