build-dev / nix-build (push) Successful in 32s
The portal's v2 API replaced nested categories (proclamation_categories.parent_id)
with flat categories plus tag-like groups: a proclamation now belongs to one
category and optionally one group. This lets the Proclamation Options page drop
its special-cased two-level tree.
Database:
- Add the proclamation_groups table (id, name, sort_order) and a
proclamations.group_id column.
- Drop parent_id from the proclamation_categories DDL so fresh databases get a
flat table.
- runMigrations() creates the new table/column, then runs a one-time
transactional v1->v2 migration keyed on PRAGMA user_version: every legacy
sub-category becomes a group (its id is preserved), proclamations that
pointed at a sub-category are remapped to (parent category, new group), and
the now-stale sub-category rows are deleted. selected flags are never
touched, so user choices survive. No network is needed, so offline kiosks
migrate at startup; re-running is a no-op.
- Add replaceGroups() and getAllGroups(); stop reading/writing parent_id.
API sync:
- Add PATH_GROUPS ("/groups") and parseGroupsJson().
- parseProclamationsJson() now also reads groupId.
- syncProclamationsFromApi() fetches /proclamations, /categories and /groups,
leaves the database untouched if any request or parse fails, then replaces
categories, replaces groups and upserts proclamations.
Proclamation Options page:
- FlatItem is now Category / Group / Proclamation.
- Each category lists only the groups that have at least one member in that
category, so impossible pairings (e.g. Physical/Prayers) never render.
- Proclamations with no group, or with a group the portal has removed, sit
directly under the category. Groups are collapsible and use a distinct color.
Tests:
- test_db: groups round-trip plus a legacy->v2 migration test asserting the
group is created, proclamations are remapped, selection is preserved and a
second initialize() is a no-op.
- test_proclamation_sync / test_api_client: parse groupId, parse groups, and
the live smoke test now also fetches /groups.
Docs: README database table and AGENTS notes updated for groups.
95 lines
3.4 KiB
C++
95 lines
3.4 KiB
C++
#pragma once
|
|
|
|
#include "page.h"
|
|
#include "db/database_manager.h"
|
|
#include "text_cache.h"
|
|
#include <SDL3_ttf/SDL_ttf.h>
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// The always-on top-level proclamation. It can neither be selected nor
|
|
// deselected, is never shown inside a category, and is always flagged as
|
|
// selected in the database. Set this to the correct proclamation ID.
|
|
constexpr const char* DEFAULT_PROPLEAMATION_ID = "f3d42354-3197-45e4-9b08-be447f14386d";
|
|
|
|
// Display name used for the always-on top-level proclamation.
|
|
constexpr const char* DEFAULT_PROPLEAMATION_NAME = "Synchronize, Harmonize & Energize";
|
|
|
|
class ProclamationOptionsPage : public Page {
|
|
public:
|
|
void onPush(AppState& state) override;
|
|
void onResume(AppState& state) override;
|
|
void onPause(AppState& state) override;
|
|
void onPop(AppState& state) override;
|
|
void handleEvent(const SDL_Event& event) override;
|
|
void update(float dt) override;
|
|
void render(SDL_Renderer* renderer) override;
|
|
|
|
private:
|
|
struct FlatItem {
|
|
enum class Type { Category, Group, Proclamation };
|
|
|
|
Type type = Type::Proclamation;
|
|
std::string id; // category id, group id, or proclamation id
|
|
std::string label; // display text
|
|
int depth = 0; // 0 = category / default, 1 = group, 2 = proclamation
|
|
bool expanded = false; // categories and groups only
|
|
bool selected = false; // proclamations only
|
|
bool isDefault = false; // the always-on top-level proclamation
|
|
};
|
|
|
|
void loadFromDb();
|
|
void rebuildItems();
|
|
void rebuildVisible();
|
|
void toggleItem();
|
|
void toggleSearchItem();
|
|
void collapseCurrent();
|
|
void expandCurrent();
|
|
void moveToParent();
|
|
void saveSelectionToDb();
|
|
void clearAllSelections();
|
|
void ensureHighlightVisible();
|
|
void scrollTo(int target);
|
|
void createFonts();
|
|
void destroyFonts();
|
|
void destroyTextures();
|
|
|
|
// Search mode. Activates on the first typed character and filters the
|
|
// flat proclamation pool (title + content) with word-prefix matching.
|
|
void enterSearchMode();
|
|
void exitSearchMode();
|
|
void rebuildSearchResults();
|
|
void appendSearchText(const char* utf8);
|
|
bool searchActive() const { return searchActive_; }
|
|
int activeListSize() const;
|
|
|
|
AppState* state_ = nullptr;
|
|
std::vector<radiantwave::db::ProclamationRecord> proclamations_;
|
|
std::vector<radiantwave::db::ProclamationCategoryRecord> categories_;
|
|
std::vector<radiantwave::db::ProclamationGroupRecord> groups_;
|
|
std::vector<FlatItem> items_;
|
|
std::vector<size_t> visible_; // indices into items_, in display order
|
|
std::vector<size_t> searchResults_; // indices into proclamations_, search mode
|
|
std::vector<std::string> searchPool_; // lowercase title + content per proclamation
|
|
int highlightIndex_ = 0; // index into the active list
|
|
int scrollOffset_ = 0;
|
|
int visibleCount_ = 8; // rows that fit on screen (recomputed each frame)
|
|
std::string statusMessage_;
|
|
|
|
bool searchActive_ = false;
|
|
std::string searchQuery_;
|
|
|
|
TTF_Font* fontTitle_ = nullptr;
|
|
TTF_Font* fontItem_ = nullptr;
|
|
TTF_Font* fontSmall_ = nullptr;
|
|
|
|
CachedText titleText_;
|
|
CachedText statusText_;
|
|
CachedText searchText_;
|
|
CachedText emptyHintText_;
|
|
CachedText footerText_;
|
|
std::vector<CachedText> rowTexts_;
|
|
std::vector<CachedText> searchRowTexts_; // indexed by proclamations_ id
|
|
};
|