feat: add OPDS server reordering with sortOrder field and numeric stepper

Servers are sorted by a persistent sortOrder field (ties broken
alphabetically). On-device editing uses a new NumericStepperActivity
with side buttons for ±1 and face buttons for ±10. The web UI gets
up/down arrow buttons and a POST /api/opds/reorder endpoint.

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-02 14:35:36 -05:00
parent 3628d8eb37
commit f955cf2fb4
18 changed files with 328 additions and 25 deletions

View File

@@ -7,6 +7,7 @@
#include <esp_mac.h>
#include <mbedtls/base64.h>
#include <algorithm>
#include <cstring>
#include "CrossPointSettings.h"
@@ -81,6 +82,7 @@ bool OpdsServerStore::saveToFile() const {
obj["username"] = server.username;
obj["password_obf"] = obfuscateToBase64(server.password);
obj["download_path"] = server.downloadPath;
obj["sort_order"] = server.sortOrder;
}
String json;
@@ -116,13 +118,32 @@ bool OpdsServerStore::loadFromFile() {
if (!server.password.empty()) needsResave = true;
}
server.downloadPath = obj["download_path"] | std::string("/");
server.sortOrder = obj["sort_order"] | 0;
if (server.sortOrder == 0) needsResave = true;
servers.push_back(std::move(server));
}
// Assign sequential sort orders to servers loaded without one
bool anyZero = false;
for (const auto& s : servers) {
if (s.sortOrder == 0) {
anyZero = true;
break;
}
}
if (anyZero) {
for (size_t i = 0; i < servers.size(); i++) {
if (servers[i].sortOrder == 0) {
servers[i].sortOrder = static_cast<int>(i) + 1;
}
}
}
sortServers();
LOG_DBG("OPS", "Loaded %zu OPDS servers from file", servers.size());
if (needsResave) {
LOG_DBG("OPS", "Resaving JSON with obfuscated passwords");
LOG_DBG("OPS", "Resaving JSON with sort_order / obfuscated passwords");
saveToFile();
}
return true;
@@ -149,6 +170,7 @@ bool OpdsServerStore::migrateFromSettings() {
server.url = SETTINGS.opdsServerUrl;
server.username = SETTINGS.opdsUsername;
server.password = SETTINGS.opdsPassword;
server.sortOrder = 1;
servers.push_back(std::move(server));
if (saveToFile()) {
@@ -171,7 +193,15 @@ bool OpdsServerStore::addServer(const OpdsServer& server) {
}
servers.push_back(server);
LOG_DBG("OPS", "Added server: %s", server.name.c_str());
if (servers.back().sortOrder == 0) {
int maxOrder = 0;
for (size_t i = 0; i + 1 < servers.size(); i++) {
maxOrder = std::max(maxOrder, servers[i].sortOrder);
}
servers.back().sortOrder = maxOrder + 1;
}
LOG_DBG("OPS", "Added server: %s (order=%d)", server.name.c_str(), servers.back().sortOrder);
sortServers();
return saveToFile();
}
@@ -181,7 +211,8 @@ bool OpdsServerStore::updateServer(size_t index, const OpdsServer& server) {
}
servers[index] = server;
LOG_DBG("OPS", "Updated server: %s", server.name.c_str());
sortServers();
LOG_DBG("OPS", "Updated server: %s (order=%d)", server.name.c_str(), server.sortOrder);
return saveToFile();
}
@@ -201,3 +232,30 @@ const OpdsServer* OpdsServerStore::getServer(size_t index) const {
}
return &servers[index];
}
bool OpdsServerStore::moveServer(size_t index, int direction) {
if (index >= servers.size()) return false;
size_t target;
if (direction < 0) {
if (index == 0) return false;
target = index - 1;
} else {
if (index + 1 >= servers.size()) return false;
target = index + 1;
}
std::swap(servers[index].sortOrder, servers[target].sortOrder);
sortServers();
return saveToFile();
}
void OpdsServerStore::sortServers() {
std::sort(servers.begin(), servers.end(), [](const OpdsServer& a, const OpdsServer& b) {
if (a.sortOrder != b.sortOrder) return a.sortOrder < b.sortOrder;
const auto& nameA = a.name.empty() ? a.url : a.name;
const auto& nameB = b.name.empty() ? b.url : b.name;
return std::lexicographical_compare(nameA.begin(), nameA.end(), nameB.begin(), nameB.end(),
[](char ca, char cb) { return tolower(ca) < tolower(cb); });
});
}