feat: port upstream OPDS improvements (PRs #1207, #1209)

Port two upstream PRs:

- PR #1207: Replace manual chunked download loop with
  HTTPClient::writeToStream via a FileWriteStream adapter, improving
  reliability for OPDS file downloads including chunked transfers.

- PR #1209: Add support for multiple OPDS servers with a new
  OpdsServerStore (JSON persistence with MAC-based password obfuscation),
  OpdsServerListActivity and OpdsSettingsActivity UIs, per-server
  credentials passed to HttpDownloader, web UI management endpoints,
  and migration from legacy single-server settings.

Made-with: Cursor
This commit is contained in:
cottongin
2026-02-26 19:14:59 -05:00
parent 19b6ad047b
commit 2aa13ea2de
30 changed files with 1119 additions and 278 deletions

View File

@@ -7,7 +7,6 @@
#include <OpdsStream.h>
#include <WiFi.h>
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "activities/network/WifiSelectionActivity.h"
#include "components/UITheme.h"
@@ -142,7 +141,8 @@ void OpdsBookBrowserActivity::render(Activity::RenderLock&&) {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_OPDS_BROWSER), true, EpdFontFamily::BOLD);
const char* headerTitle = server.name.empty() ? tr(STR_OPDS_BROWSER) : server.name.c_str();
renderer.drawCenteredText(UI_12_FONT_ID, 15, headerTitle, true, EpdFontFamily::BOLD);
if (state == BrowserState::CHECK_WIFI) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str());
@@ -171,7 +171,9 @@ void OpdsBookBrowserActivity::render(Activity::RenderLock&&) {
if (state == BrowserState::DOWNLOADING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 40, tr(STR_DOWNLOADING));
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 10, statusMessage.c_str());
const auto maxWidth = pageWidth - 40;
auto title = renderer.truncatedText(UI_10_FONT_ID, statusMessage.c_str(), maxWidth);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 10, title.c_str());
if (downloadTotal > 0) {
const int barWidth = pageWidth - 100;
constexpr int barHeight = 20;
@@ -225,22 +227,21 @@ void OpdsBookBrowserActivity::render(Activity::RenderLock&&) {
}
void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
const char* serverUrl = SETTINGS.opdsServerUrl;
if (strlen(serverUrl) == 0) {
if (server.url.empty()) {
state = BrowserState::ERROR;
errorMessage = tr(STR_NO_SERVER_URL);
requestUpdate();
return;
}
std::string url = UrlUtils::buildUrl(serverUrl, path);
std::string url = UrlUtils::buildUrl(server.url, path);
LOG_DBG("OPDS", "Fetching: %s", url.c_str());
OpdsParser parser;
{
OpdsParserStream stream{parser};
if (!HttpDownloader::fetchUrl(url, stream)) {
if (!HttpDownloader::fetchUrl(url, stream, server.username, server.password)) {
state = BrowserState::ERROR;
errorMessage = tr(STR_FETCH_FEED_FAILED);
requestUpdate();
@@ -311,7 +312,7 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
requestUpdate();
// Build full download URL
std::string downloadUrl = UrlUtils::buildUrl(SETTINGS.opdsServerUrl, book.href);
std::string downloadUrl = UrlUtils::buildUrl(server.url, book.href);
// Create sanitized filename: "Title - Author.epub" or just "Title.epub" if no author
std::string baseName = book.title;
@@ -322,12 +323,14 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
LOG_DBG("OPDS", "Downloading: %s -> %s", downloadUrl.c_str(), filename.c_str());
const auto result =
HttpDownloader::downloadToFile(downloadUrl, filename, [this](const size_t downloaded, const size_t total) {
const auto result = HttpDownloader::downloadToFile(
downloadUrl, filename,
[this](const size_t downloaded, const size_t total) {
downloadProgress = downloaded;
downloadTotal = total;
requestUpdate();
});
},
server.username, server.password);
if (result == HttpDownloader::OK) {
LOG_DBG("OPDS", "Download complete: %s", filename.c_str());

View File

@@ -6,6 +6,7 @@
#include <vector>
#include "../ActivityWithSubactivity.h"
#include "OpdsServerStore.h"
#include "util/ButtonNavigator.h"
/**
@@ -25,8 +26,8 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity {
};
explicit OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onGoHome)
: ActivityWithSubactivity("OpdsBookBrowser", renderer, mappedInput), onGoHome(onGoHome) {}
const std::function<void()>& onGoHome, const OpdsServer& server)
: ActivityWithSubactivity("OpdsBookBrowser", renderer, mappedInput), onGoHome(onGoHome), server(server) {}
void onEnter() override;
void onExit() override;
@@ -46,6 +47,7 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity {
size_t downloadTotal = 0;
const std::function<void()> onGoHome;
OpdsServer server; // Copied at construction — safe even if the store changes during browsing
void checkAndConnectWifi();
void launchWifiSelection();

View File

@@ -17,6 +17,7 @@
#include "CrossPointSettings.h"
#include "CrossPointState.h"
#include "MappedInputManager.h"
#include "OpdsServerStore.h"
#include "RecentBooksStore.h"
#include "components/UITheme.h"
#include "fontIds.h"
@@ -28,7 +29,7 @@ int HomeActivity::getMenuItemCount() const {
if (!recentBooks.empty()) {
count += recentBooks.size();
}
if (hasOpdsUrl) {
if (hasOpdsServers) {
count++;
}
return count;
@@ -128,8 +129,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
void HomeActivity::onEnter() {
ActivityWithSubactivity::onEnter();
// Check if OPDS browser URL is configured
hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0;
hasOpdsServers = OPDS_STORE.hasServers();
selectorIndex = 0;
@@ -238,7 +238,7 @@ void HomeActivity::loop() {
int menuSelectedIndex = selectorIndex - static_cast<int>(recentBooks.size());
const int myLibraryIdx = idx++;
const int recentsIdx = idx++;
const int opdsLibraryIdx = hasOpdsUrl ? idx++ : -1;
const int opdsLibraryIdx = hasOpdsServers ? idx++ : -1;
const int fileTransferIdx = idx++;
const int settingsIdx = idx;
@@ -277,7 +277,7 @@ void HomeActivity::render(Activity::RenderLock&&) {
tr(STR_SETTINGS_TITLE)};
std::vector<UIIcon> menuIcons = {Folder, Recent, Transfer, Settings};
if (hasOpdsUrl) {
if (hasOpdsServers) {
// Insert OPDS Browser after My Library
menuItems.insert(menuItems.begin() + 2, tr(STR_OPDS_BROWSER));
menuIcons.insert(menuIcons.begin() + 2, Library);

View File

@@ -15,7 +15,7 @@ class HomeActivity final : public ActivityWithSubactivity {
bool recentsLoading = false;
bool recentsLoaded = false;
bool firstRenderDone = false;
bool hasOpdsUrl = false;
bool hasOpdsServers = false;
bool coverRendered = false; // Track if cover has been rendered once
bool coverBufferStored = false; // Track if cover buffer is stored
uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image

View File

@@ -1,149 +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() {
ActivityWithSubactivity::onEnter();
selectedIndex = 0;
requestUpdate();
}
void CalibreSettingsActivity::onExit() { ActivityWithSubactivity::onExit(); }
void CalibreSettingsActivity::loop() {
if (subActivity) {
subActivity->loop();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onBack();
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
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_CALIBRE_WEB_URL), SETTINGS.opdsServerUrl,
127, // maxLength
false, // not password
[this](const std::string& url) {
strncpy(SETTINGS.opdsServerUrl, url.c_str(), sizeof(SETTINGS.opdsServerUrl) - 1);
SETTINGS.opdsServerUrl[sizeof(SETTINGS.opdsServerUrl) - 1] = '\0';
SETTINGS.saveToFile();
exitActivity();
requestUpdate();
},
[this]() {
exitActivity();
requestUpdate();
}));
} else if (selectedIndex == 1) {
// Username
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_USERNAME), SETTINGS.opdsUsername,
63, // maxLength
false, // not password
[this](const std::string& username) {
strncpy(SETTINGS.opdsUsername, username.c_str(), sizeof(SETTINGS.opdsUsername) - 1);
SETTINGS.opdsUsername[sizeof(SETTINGS.opdsUsername) - 1] = '\0';
SETTINGS.saveToFile();
exitActivity();
requestUpdate();
},
[this]() {
exitActivity();
requestUpdate();
}));
} else if (selectedIndex == 2) {
// Password
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_PASSWORD), SETTINGS.opdsPassword,
63, // maxLength
false, // not password mode
[this](const std::string& password) {
strncpy(SETTINGS.opdsPassword, password.c_str(), sizeof(SETTINGS.opdsPassword) - 1);
SETTINGS.opdsPassword[sizeof(SETTINGS.opdsPassword) - 1] = '\0';
SETTINGS.saveToFile();
exitActivity();
requestUpdate();
},
[this]() {
exitActivity();
requestUpdate();
}));
}
}
void CalibreSettingsActivity::render(Activity::RenderLock&&) {
renderer.clearScreen();
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);
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();
}

View File

@@ -1,29 +0,0 @@
#pragma once
#include <functional>
#include "activities/ActivityWithSubactivity.h"
#include "util/ButtonNavigator.h"
/**
* Submenu for OPDS Browser settings.
* Shows OPDS Server URL and HTTP authentication options.
*/
class CalibreSettingsActivity final : public ActivityWithSubactivity {
public:
explicit CalibreSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onBack)
: ActivityWithSubactivity("CalibreSettings", renderer, mappedInput), onBack(onBack) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
private:
ButtonNavigator buttonNavigator;
size_t selectedIndex = 0;
const std::function<void()> onBack;
void handleSelection();
};

View File

@@ -0,0 +1,131 @@
#include "OpdsServerListActivity.h"
#include <GfxRenderer.h>
#include <I18n.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());
// In settings mode, append a virtual "Add Server" item; in picker mode, only show real servers
if (!isPickerMode()) {
count++;
}
return count;
}
void OpdsServerListActivity::onEnter() {
ActivityWithSubactivity::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() { ActivityWithSubactivity::onExit(); }
void OpdsServerListActivity::loop() {
if (subActivity) {
subActivity->loop();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onBack();
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 (isPickerMode()) {
// Picker mode: selecting a server triggers the callback instead of opening the editor
if (selectedIndex < serverCount) {
onServerSelected(static_cast<size_t>(selectedIndex));
}
return;
}
// Settings mode: open editor for selected server, or create a new one
auto onEditDone = [this] {
exitActivity();
selectedIndex = 0;
requestUpdate();
};
if (selectedIndex < serverCount) {
exitActivity();
enterNewActivity(new OpdsSettingsActivity(renderer, mappedInput, onEditDone, selectedIndex));
} else {
exitActivity();
enterNewActivity(new OpdsSettingsActivity(renderer, mappedInput, onEditDone, -1));
}
}
void OpdsServerListActivity::render(Activity::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();
}

View File

@@ -0,0 +1,41 @@
#pragma once
#include <functional>
#include "activities/ActivityWithSubactivity.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.
*/
class OpdsServerListActivity final : public ActivityWithSubactivity {
public:
using OnServerSelected = std::function<void(size_t serverIndex)>;
/**
* @param onBack Called when user presses Back
* @param onServerSelected If set, acts as a picker: selecting a server calls this instead of opening editor.
*/
explicit OpdsServerListActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onBack, OnServerSelected onServerSelected = nullptr)
: ActivityWithSubactivity("OpdsServerList", renderer, mappedInput),
onBack(onBack),
onServerSelected(std::move(onServerSelected)) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
private:
ButtonNavigator buttonNavigator;
int selectedIndex = 0;
const std::function<void()> onBack;
OnServerSelected onServerSelected;
bool isPickerMode() const { return onServerSelected != nullptr; }
int getItemCount() const;
void handleSelection();
};

View File

@@ -0,0 +1,199 @@
#include "OpdsSettingsActivity.h"
#include <GfxRenderer.h>
#include <I18n.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() {
ActivityWithSubactivity::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() { ActivityWithSubactivity::onExit(); }
void OpdsSettingsActivity::loop() {
if (subActivity) {
subActivity->loop();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onBack();
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);
// After the first field is saved, 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 {
OPDS_STORE.updateServer(static_cast<size_t>(serverIndex), editServer);
}
}
void OpdsSettingsActivity::handleSelection() {
if (selectedIndex == 0) {
// Server Name
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_SERVER_NAME), editServer.name, 63, false,
[this](const std::string& name) {
editServer.name = name;
saveServer();
exitActivity();
requestUpdate();
},
[this]() {
exitActivity();
requestUpdate();
}));
} else if (selectedIndex == 1) {
// Server URL
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_OPDS_SERVER_URL), editServer.url, 127, false,
[this](const std::string& url) {
editServer.url = url;
saveServer();
exitActivity();
requestUpdate();
},
[this]() {
exitActivity();
requestUpdate();
}));
} else if (selectedIndex == 2) {
// Username
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_USERNAME), editServer.username, 63, false,
[this](const std::string& username) {
editServer.username = username;
saveServer();
exitActivity();
requestUpdate();
},
[this]() {
exitActivity();
requestUpdate();
}));
} else if (selectedIndex == 3) {
// Password
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_PASSWORD), editServer.password, 63, false,
[this](const std::string& password) {
editServer.password = password;
saveServer();
exitActivity();
requestUpdate();
},
[this]() {
exitActivity();
requestUpdate();
}));
} else if (selectedIndex == 4 && !isNewServer) {
// Delete server
OPDS_STORE.removeServer(static_cast<size_t>(serverIndex));
onBack();
}
}
void OpdsSettingsActivity::render(Activity::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_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);
renderer.displayBuffer();
}

View File

@@ -0,0 +1,40 @@
#pragma once
#include <functional>
#include "OpdsServerStore.h"
#include "activities/ActivityWithSubactivity.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 ActivityWithSubactivity {
public:
/**
* @param serverIndex Index into OpdsServerStore, or -1 for a new server
*/
explicit OpdsSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onBack, int serverIndex = -1)
: ActivityWithSubactivity("OpdsSettings", renderer, mappedInput), onBack(onBack), serverIndex(serverIndex) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
private:
ButtonNavigator buttonNavigator;
size_t selectedIndex = 0;
const std::function<void()> onBack;
int serverIndex;
OpdsServer editServer;
bool isNewServer = false;
int getMenuItemCount() const;
void handleSelection();
void saveServer();
};

View File

@@ -7,7 +7,7 @@
#include <cstdlib>
#include "ButtonRemapActivity.h"
#include "CalibreSettingsActivity.h"
#include "OpdsServerListActivity.h"
#include "ClearCacheActivity.h"
#include "CrossPointSettings.h"
#include "KOReaderSettingsActivity.h"
@@ -202,7 +202,7 @@ void SettingsActivity::toggleCurrentSetting() {
enterSubActivity(new KOReaderSettingsActivity(renderer, mappedInput, onComplete));
break;
case SettingAction::OPDSBrowser:
enterSubActivity(new CalibreSettingsActivity(renderer, mappedInput, onComplete));
enterSubActivity(new OpdsServerListActivity(renderer, mappedInput, onComplete));
break;
case SettingAction::Network:
enterSubActivity(new WifiSelectionActivity(renderer, mappedInput, onCompleteBool, false));