Kiosk: System Name entry in System Settings
Adds a System Name text-entry page (hint 'e.g. Living Room', 1-40 char validation) and a data-driven System Settings row for it. Saving persists locally and nudges the background sync; conflict/invalid results surface the message and restore the previous name. Extends the settings-menu tests.
This commit is contained in:
@@ -42,6 +42,7 @@ add_executable(radiantwave
|
||||
src/settings.cpp
|
||||
src/settings_menu.cpp
|
||||
src/system_settings_page.cpp
|
||||
src/system_name_page.cpp
|
||||
src/wifi_setup_page.cpp
|
||||
src/audio_settings_page.cpp
|
||||
src/audio/audio_manager.cpp
|
||||
|
||||
@@ -6,6 +6,7 @@ std::vector<SystemSettingEntry> systemSettingEntries() {
|
||||
return {
|
||||
{"Email Address", SystemSettingId::EmailAddress},
|
||||
{"License Key", SystemSettingId::LicenseKey},
|
||||
{"System Name", SystemSettingId::SystemName},
|
||||
{"WiFi Setup", SystemSettingId::WiFiSetup},
|
||||
{"Time Zone Setup", SystemSettingId::Timezone},
|
||||
{"Date Format", SystemSettingId::DateFormat},
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace radiantwave::settings {
|
||||
enum class SystemSettingId {
|
||||
EmailAddress,
|
||||
LicenseKey,
|
||||
SystemName,
|
||||
WiFiSetup,
|
||||
Timezone,
|
||||
DateFormat,
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
#include "system_name_page.h"
|
||||
#include "app_state.h"
|
||||
#include "network/system_name.h"
|
||||
#include "logical_size.h"
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t MAX_INPUT_LENGTH = 80;
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
void SystemNamePage::onPush(AppState& state)
|
||||
{
|
||||
state_ = &state;
|
||||
TTF_Init();
|
||||
createFonts();
|
||||
|
||||
storedValue_ = radiantwave::systemname::storedName(state.db);
|
||||
input_ = storedValue_;
|
||||
statusMessage_ = "Enter a System Name, then press ENTER";
|
||||
statusIsError_ = false;
|
||||
|
||||
if (state.window) {
|
||||
SDL_StartTextInput(state.window);
|
||||
}
|
||||
}
|
||||
|
||||
void SystemNamePage::onResume(AppState& state)
|
||||
{
|
||||
state_ = &state;
|
||||
createFonts();
|
||||
storedValue_ = radiantwave::systemname::storedName(state.db);
|
||||
input_ = storedValue_;
|
||||
|
||||
if (state.window) {
|
||||
SDL_StartTextInput(state.window);
|
||||
}
|
||||
}
|
||||
|
||||
void SystemNamePage::onPause(AppState& state)
|
||||
{
|
||||
state.keybinds.unbindAll();
|
||||
if (state.window) {
|
||||
SDL_StopTextInput(state.window);
|
||||
}
|
||||
destroyFonts();
|
||||
destroyTextures();
|
||||
}
|
||||
|
||||
void SystemNamePage::onPop(AppState& state)
|
||||
{
|
||||
if (state.window) {
|
||||
SDL_StopTextInput(state.window);
|
||||
}
|
||||
destroyFonts();
|
||||
destroyTextures();
|
||||
TTF_Quit();
|
||||
}
|
||||
|
||||
void SystemNamePage::createFonts()
|
||||
{
|
||||
destroyFonts();
|
||||
fontTitle_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 52);
|
||||
fontHeader_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 28);
|
||||
fontBody_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 32);
|
||||
fontSmall_ = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 22);
|
||||
}
|
||||
|
||||
void SystemNamePage::destroyFonts()
|
||||
{
|
||||
if (fontTitle_) { TTF_CloseFont(fontTitle_); fontTitle_ = nullptr; }
|
||||
if (fontHeader_) { TTF_CloseFont(fontHeader_); fontHeader_ = nullptr; }
|
||||
if (fontBody_) { TTF_CloseFont(fontBody_); fontBody_ = nullptr; }
|
||||
if (fontSmall_) { TTF_CloseFont(fontSmall_); fontSmall_ = nullptr; }
|
||||
}
|
||||
|
||||
void SystemNamePage::destroyTextures()
|
||||
{
|
||||
titleText_.destroy();
|
||||
storedText_.destroy();
|
||||
valueText_.destroy();
|
||||
statusText_.destroy();
|
||||
hintText_.destroy();
|
||||
}
|
||||
|
||||
void SystemNamePage::submit()
|
||||
{
|
||||
if (!state_) return;
|
||||
|
||||
// Empty/whitespace is invalid and never clears the stored name.
|
||||
std::string error;
|
||||
if (!radiantwave::systemname::storeLocalName(state_->db, input_, &error)) {
|
||||
statusMessage_ = error;
|
||||
statusIsError_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
storedValue_ = radiantwave::systemname::storedName(state_->db);
|
||||
input_ = storedValue_;
|
||||
statusMessage_ = "Saved. Syncing with the portal...";
|
||||
statusIsError_ = false;
|
||||
|
||||
// Never block the UI: the background service performs the PUT and retries.
|
||||
state_->systemNameSync.requestSync();
|
||||
}
|
||||
|
||||
void SystemNamePage::pollSyncResult()
|
||||
{
|
||||
if (!state_) return;
|
||||
const std::string message = radiantwave::systemname::consumeMessage(state_->db);
|
||||
if (message.empty()) return;
|
||||
|
||||
statusMessage_ = message;
|
||||
statusIsError_ = true;
|
||||
storedValue_ = radiantwave::systemname::storedName(state_->db);
|
||||
input_ = storedValue_;
|
||||
}
|
||||
|
||||
void SystemNamePage::handleEvent(const SDL_Event& event)
|
||||
{
|
||||
if (!state_) return;
|
||||
|
||||
if (event.type == SDL_EVENT_TEXT_INPUT) {
|
||||
if (input_.size() < MAX_INPUT_LENGTH) {
|
||||
input_ += event.text.text;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_KEY_DOWN) {
|
||||
SDL_Keycode key = event.key.key;
|
||||
|
||||
if (key == SDLK_BACKSPACE) {
|
||||
if (!input_.empty()) {
|
||||
input_.pop_back();
|
||||
}
|
||||
} else if (key == SDLK_RETURN || key == SDLK_KP_ENTER) {
|
||||
submit();
|
||||
} else if (key == SDLK_ESCAPE) {
|
||||
state_->pages.pop(*state_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SystemNamePage::update(float dt)
|
||||
{
|
||||
pollSyncResult();
|
||||
}
|
||||
|
||||
void SystemNamePage::render(SDL_Renderer* renderer)
|
||||
{
|
||||
int winW, winH;
|
||||
radiantwave::getLayoutSize(renderer, &winW, &winH);
|
||||
float fw = static_cast<float>(winW);
|
||||
float fh = static_cast<float>(winH);
|
||||
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderDrawColor(renderer, 20, 20, 30, 255);
|
||||
SDL_FRect bgRect{0, 0, fw, fh};
|
||||
SDL_RenderFillRect(renderer, &bgRect);
|
||||
|
||||
if (!fontTitle_ || !fontHeader_ || !fontBody_ || !fontSmall_) return;
|
||||
|
||||
SDL_Color white = {255, 255, 255, 255};
|
||||
SDL_Color yellow = {255, 255, 0, 255};
|
||||
SDL_Color cyan = {100, 220, 255, 255};
|
||||
SDL_Color gray = {180, 180, 180, 255};
|
||||
SDL_Color red = {240, 100, 100, 255};
|
||||
|
||||
float currentY = 30.0f;
|
||||
|
||||
float boxW = 700.0f;
|
||||
float boxH = 70.0f;
|
||||
float boxX = (fw - boxW) / 2.0f;
|
||||
|
||||
titleText_.ensure(renderer, fontTitle_, "System Name", white);
|
||||
if (titleText_.texture) {
|
||||
float x = (fw - titleText_.width) / 2.0f;
|
||||
SDL_FRect dst{x, currentY, titleText_.width, titleText_.height};
|
||||
SDL_RenderTexture(renderer, titleText_.texture, nullptr, &dst);
|
||||
currentY += titleText_.height + 30.0f;
|
||||
}
|
||||
|
||||
std::string storedStr = storedValue_.empty() ? "Not set" : storedValue_;
|
||||
storedText_.ensure(renderer, fontHeader_, "Saved: " + storedStr,
|
||||
storedValue_.empty() ? gray : yellow);
|
||||
if (storedText_.texture) {
|
||||
SDL_FRect dst{boxX, currentY, storedText_.width, storedText_.height};
|
||||
SDL_RenderTexture(renderer, storedText_.texture, nullptr, &dst);
|
||||
}
|
||||
currentY += 50.0f;
|
||||
|
||||
SDL_SetRenderDrawColor(renderer, 35, 40, 60, 255);
|
||||
SDL_FRect boxRect{boxX, currentY, boxW, boxH};
|
||||
SDL_RenderFillRect(renderer, &boxRect);
|
||||
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
|
||||
SDL_RenderRect(renderer, &boxRect);
|
||||
|
||||
std::string display = input_ + "_";
|
||||
valueText_.ensure(renderer, fontBody_, display, cyan);
|
||||
if (valueText_.texture) {
|
||||
SDL_FRect dst{boxX + 20.0f, currentY + (boxH - valueText_.height) / 2.0f,
|
||||
valueText_.width, valueText_.height};
|
||||
SDL_RenderTexture(renderer, valueText_.texture, nullptr, &dst);
|
||||
}
|
||||
currentY += boxH + 30.0f;
|
||||
|
||||
if (!statusMessage_.empty()) {
|
||||
statusText_.ensure(renderer, fontHeader_, statusMessage_,
|
||||
statusIsError_ ? red : gray);
|
||||
if (statusText_.texture) {
|
||||
SDL_FRect dst{boxX, currentY, statusText_.width, statusText_.height};
|
||||
SDL_RenderTexture(renderer, statusText_.texture, nullptr, &dst);
|
||||
}
|
||||
currentY += 50.0f;
|
||||
}
|
||||
|
||||
hintText_.ensure(renderer, fontSmall_,
|
||||
"e.g. Living Room [Type] Enter [ENTER] Save [BACKSPACE] Delete [ESC] Back", gray);
|
||||
if (hintText_.texture) {
|
||||
float x = (fw - hintText_.width) / 2.0f;
|
||||
SDL_FRect dst{x, fh - 40.0f, hintText_.width, hintText_.height};
|
||||
SDL_RenderTexture(renderer, hintText_.texture, nullptr, &dst);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "page.h"
|
||||
#include "text_cache.h"
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
#include <string>
|
||||
|
||||
// Text-entry page for the Kiosk's System Name (a human-readable label for this
|
||||
// License Key). Saves locally first, then syncs in the background so the UI is
|
||||
// never blocked on the network.
|
||||
class SystemNamePage : 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:
|
||||
void createFonts();
|
||||
void destroyFonts();
|
||||
void destroyTextures();
|
||||
void submit();
|
||||
void pollSyncResult();
|
||||
|
||||
AppState* state_ = nullptr;
|
||||
std::string storedValue_;
|
||||
std::string input_;
|
||||
std::string statusMessage_;
|
||||
bool statusIsError_ = false;
|
||||
|
||||
TTF_Font* fontTitle_ = nullptr;
|
||||
TTF_Font* fontHeader_ = nullptr;
|
||||
TTF_Font* fontBody_ = nullptr;
|
||||
TTF_Font* fontSmall_ = nullptr;
|
||||
|
||||
CachedText titleText_;
|
||||
CachedText storedText_;
|
||||
CachedText valueText_;
|
||||
CachedText statusText_;
|
||||
CachedText hintText_;
|
||||
};
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "logical_size.h"
|
||||
#include "timezone_page.h"
|
||||
#include "wifi_setup_page.h"
|
||||
#include "system_name_page.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <algorithm>
|
||||
@@ -76,6 +77,9 @@ void SystemSettingsPage::activateSelected(AppState &state)
|
||||
case radiantwave::settings::SystemSettingId::LicenseKey:
|
||||
state.pages.push(state, std::make_unique<LicenseKeyPage>());
|
||||
break;
|
||||
case radiantwave::settings::SystemSettingId::SystemName:
|
||||
state.pages.push(state, std::make_unique<SystemNamePage>());
|
||||
break;
|
||||
case radiantwave::settings::SystemSettingId::WiFiSetup:
|
||||
state.pages.push(state, std::make_unique<WiFiSetupPage>());
|
||||
break;
|
||||
|
||||
@@ -7,17 +7,19 @@ namespace settings = radiantwave::settings;
|
||||
|
||||
TEST(SystemSettingsMenuTest, ListsTheSystemSettingsInOrder) {
|
||||
const auto entries = settings::systemSettingEntries();
|
||||
ASSERT_EQ(entries.size(), 5u);
|
||||
ASSERT_EQ(entries.size(), 6u);
|
||||
EXPECT_EQ(entries[0].label, "Email Address");
|
||||
EXPECT_EQ(entries[0].id, settings::SystemSettingId::EmailAddress);
|
||||
EXPECT_EQ(entries[1].label, "License Key");
|
||||
EXPECT_EQ(entries[1].id, settings::SystemSettingId::LicenseKey);
|
||||
EXPECT_EQ(entries[2].label, "WiFi Setup");
|
||||
EXPECT_EQ(entries[2].id, settings::SystemSettingId::WiFiSetup);
|
||||
EXPECT_EQ(entries[3].label, "Time Zone Setup");
|
||||
EXPECT_EQ(entries[3].id, settings::SystemSettingId::Timezone);
|
||||
EXPECT_EQ(entries[4].label, "Date Format");
|
||||
EXPECT_EQ(entries[4].id, settings::SystemSettingId::DateFormat);
|
||||
EXPECT_EQ(entries[2].label, "System Name");
|
||||
EXPECT_EQ(entries[2].id, settings::SystemSettingId::SystemName);
|
||||
EXPECT_EQ(entries[3].label, "WiFi Setup");
|
||||
EXPECT_EQ(entries[3].id, settings::SystemSettingId::WiFiSetup);
|
||||
EXPECT_EQ(entries[4].label, "Time Zone Setup");
|
||||
EXPECT_EQ(entries[4].id, settings::SystemSettingId::Timezone);
|
||||
EXPECT_EQ(entries[5].label, "Date Format");
|
||||
EXPECT_EQ(entries[5].id, settings::SystemSettingId::DateFormat);
|
||||
}
|
||||
|
||||
TEST(SystemSettingsMenuTest, LabelsAreNonEmptyAndUnique) {
|
||||
@@ -38,6 +40,7 @@ TEST(SystemSettingsMenuTest, EveryEntryMapsToAKnownPage) {
|
||||
switch (entry.id) {
|
||||
case settings::SystemSettingId::EmailAddress:
|
||||
case settings::SystemSettingId::LicenseKey:
|
||||
case settings::SystemSettingId::SystemName:
|
||||
case settings::SystemSettingId::WiFiSetup:
|
||||
case settings::SystemSettingId::Timezone:
|
||||
case settings::SystemSettingId::DateFormat:
|
||||
|
||||
Reference in New Issue
Block a user