Files
Crosspoint/src/activities/settings/OpdsSettingsActivity.cpp
T
2026-04-23 17:08:08 +02:00

242 lines
8.3 KiB
C++

#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;
constexpr char INVALID_OPDS_URL_MESSAGE[] = "Enter a valid OPDS URL";
} // 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;
popupMessage.clear();
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.
const auto insertedIndex = OPDS_STORE.addServer(editServer);
success = insertedIndex.has_value();
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>(*insertedIndex);
} 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 (success) {
popupMessage.clear();
}
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,
OpdsServerStore::MAX_NAME_LENGTH, 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);
const auto normalizedUrl = OpdsServerValidation::normalizeUrl(kb.text);
if (!normalizedUrl) {
popupMessage = INVALID_OPDS_URL_MESSAGE;
requestUpdate();
return;
}
popupMessage.clear();
editServer.url = *normalizedUrl;
saveServer();
requestUpdate();
}
};
startActivityForResult(
std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_OPDS_SERVER_URL), prefillUrl,
OpdsServerStore::MAX_URL_LENGTH, 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,
OpdsServerStore::MAX_USERNAME_LENGTH, 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,
OpdsServerStore::MAX_PASSWORD_LENGTH, 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 (!popupMessage.empty()) {
GUI.drawPopup(renderer, popupMessage.c_str());
} else if (showSaveError) {
GUI.drawPopup(renderer, tr(STR_ERROR_GENERAL_FAILURE));
}
renderer.displayBuffer();
}