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:
99
src/activities/util/NumericStepperActivity.cpp
Normal file
99
src/activities/util/NumericStepperActivity.cpp
Normal 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();
|
||||
}
|
||||
42
src/activities/util/NumericStepperActivity.h
Normal file
42
src/activities/util/NumericStepperActivity.h
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user