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

@@ -0,0 +1,99 @@
#include "NumericStepperActivity.h"
#include <GfxRenderer.h>
#include <I18n.h>
#include <algorithm>
#include <cstdio>
#include "MappedInputManager.h"
#include "components/UITheme.h"
#include "fontIds.h"
void NumericStepperActivity::onEnter() {
Activity::onEnter();
requestUpdate();
}
void NumericStepperActivity::onExit() { Activity::onExit(); }
void NumericStepperActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
if (onCancel) onCancel();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (onComplete) onComplete(value);
return;
}
// Side buttons: ±1
if (mappedInput.wasPressed(MappedInputManager::Button::Up)) {
if (value < maxValue) {
value++;
requestUpdate();
}
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Down)) {
if (value > minValue) {
value--;
requestUpdate();
}
return;
}
// Front face buttons: ±10
if (mappedInput.wasPressed(MappedInputManager::Button::Right)) {
value = std::min(value + 10, maxValue);
requestUpdate();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Left)) {
value = std::max(value - 10, minValue);
requestUpdate();
return;
}
}
void NumericStepperActivity::render(Activity::RenderLock&&) {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
const int lineHeight12 = renderer.getLineHeight(UI_12_FONT_ID);
const auto& metrics = UITheme::getInstance().getMetrics();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, title.c_str());
char valueStr[16];
snprintf(valueStr, sizeof(valueStr), "%d", value);
const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, valueStr);
const int startX = (pageWidth - textWidth) / 2;
const int valueY = metrics.topPadding + metrics.headerHeight + 60;
constexpr int highlightPad = 10;
renderer.fillRoundedRect(startX - highlightPad, valueY - 4, textWidth + highlightPad * 2, lineHeight12 + 8, 6,
Color::LightGray);
renderer.drawText(UI_12_FONT_ID, startX, valueY, valueStr, true);
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);
}
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SAVE), "-10", "+10");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
GUI.drawSideButtonHints(renderer, "+1", "-1");
renderer.displayBuffer();
}

View File

@@ -0,0 +1,42 @@
#pragma once
#include <functional>
#include <string>
#include <utility>
#include "activities/Activity.h"
/**
* Reusable numeric stepper for integer value entry.
* Side buttons (Up/Down) step by 1, face buttons (Left/Right) step by 10.
* Value is clamped within [min, max].
*/
class NumericStepperActivity final : public Activity {
public:
using OnCompleteCallback = std::function<void(int)>;
using OnCancelCallback = std::function<void()>;
explicit NumericStepperActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string title,
int initialValue, int minValue, int maxValue, OnCompleteCallback onComplete,
OnCancelCallback onCancel)
: Activity("NumericStepper", renderer, mappedInput),
title(std::move(title)),
value(initialValue),
minValue(minValue),
maxValue(maxValue),
onComplete(std::move(onComplete)),
onCancel(std::move(onCancel)) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
private:
std::string title;
int value;
int minValue;
int maxValue;
OnCompleteCallback onComplete;
OnCancelCallback onCancel;
};