ScrollerPage now resolves the active Block from ScheduleManager (reusing the existing stream-switch path), honoring dynamicAudio true/false and falling back to local selection for gaps, no schedule, or toggle off. Adds a 'Schedule' On/Off entry (default On) in System Settings persisted in config.yaml, and config plumbing (proclamation.schedule_enabled). All on-device schedule times remain 12h AM/PM.
281 lines
8.2 KiB
C++
281 lines
8.2 KiB
C++
#include "system_settings_page.h"
|
|
#include "app_state.h"
|
|
#include "date_format_page.h"
|
|
#include "email_address_page.h"
|
|
#include "license_key_page.h"
|
|
#include "logical_size.h"
|
|
#include "timezone_page.h"
|
|
#include "wifi_setup_page.h"
|
|
#include "system_name_page.h"
|
|
#include "schedule_toggle_page.h"
|
|
#include <SDL3/SDL.h>
|
|
#include <SDL3_image/SDL_image.h>
|
|
#include <algorithm>
|
|
|
|
namespace
|
|
{
|
|
constexpr const char *TITLE = "System Settings";
|
|
constexpr const char *BACK_LABEL = "Back to Settings";
|
|
} // anonymous namespace
|
|
|
|
void SystemSettingsPage::onPush(AppState &state)
|
|
{
|
|
state_ = &state;
|
|
TTF_Init();
|
|
createTextures(state);
|
|
bindNavigation(state);
|
|
}
|
|
|
|
void SystemSettingsPage::onResume(AppState &state)
|
|
{
|
|
state_ = &state;
|
|
createTextures(state);
|
|
bindNavigation(state);
|
|
}
|
|
|
|
void SystemSettingsPage::onPause(AppState &state)
|
|
{
|
|
state.keybinds.unbindAll();
|
|
destroyTextures();
|
|
}
|
|
|
|
void SystemSettingsPage::onPop(AppState &state)
|
|
{
|
|
destroyTextures();
|
|
TTF_Quit();
|
|
}
|
|
|
|
void SystemSettingsPage::bindNavigation(AppState &state)
|
|
{
|
|
state.keybinds.bind(SDLK_UP, "system_settings_up", [this]()
|
|
{
|
|
int count = static_cast<int>(items_.size());
|
|
if (count > 0)
|
|
selectedItem_ = (selectedItem_ - 1 + count) % count;
|
|
});
|
|
state.keybinds.bind(SDLK_DOWN, "system_settings_down", [this]()
|
|
{
|
|
int count = static_cast<int>(items_.size());
|
|
if (count > 0)
|
|
selectedItem_ = (selectedItem_ + 1) % count;
|
|
});
|
|
}
|
|
|
|
void SystemSettingsPage::activateSelected(AppState &state)
|
|
{
|
|
// The trailing row (index == number of setting entries) goes back.
|
|
if (selectedItem_ < 0 || selectedItem_ >= static_cast<int>(entries_.size()))
|
|
{
|
|
state.pages.pop(state);
|
|
return;
|
|
}
|
|
|
|
switch (entries_[selectedItem_].id)
|
|
{
|
|
case radiantwave::settings::SystemSettingId::EmailAddress:
|
|
state.pages.push(state, std::make_unique<EmailAddressPage>());
|
|
break;
|
|
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::Schedule:
|
|
state.pages.push(state, std::make_unique<ScheduleTogglePage>());
|
|
break;
|
|
case radiantwave::settings::SystemSettingId::WiFiSetup:
|
|
state.pages.push(state, std::make_unique<WiFiSetupPage>());
|
|
break;
|
|
case radiantwave::settings::SystemSettingId::Timezone:
|
|
state.pages.push(state, std::make_unique<TimeZonePage>());
|
|
break;
|
|
case radiantwave::settings::SystemSettingId::DateFormat:
|
|
state.pages.push(state, std::make_unique<DateFormatPage>());
|
|
break;
|
|
}
|
|
}
|
|
|
|
void SystemSettingsPage::handleEvent(const SDL_Event &event)
|
|
{
|
|
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_RETURN)
|
|
{
|
|
activateSelected(*state_);
|
|
}
|
|
else if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE)
|
|
{
|
|
state_->pages.pop(*state_);
|
|
}
|
|
}
|
|
|
|
void SystemSettingsPage::update(float dt)
|
|
{
|
|
}
|
|
|
|
void SystemSettingsPage::render(SDL_Renderer *renderer)
|
|
{
|
|
int winW, winH;
|
|
radiantwave::getLayoutSize(renderer, &winW, &winH);
|
|
float fw = static_cast<float>(winW);
|
|
float fh = static_cast<float>(winH);
|
|
|
|
// 1. Logo Background Image
|
|
if (bgTexture_)
|
|
{
|
|
float scaleX = fw / bgWidth_;
|
|
float scaleY = fh / bgHeight_;
|
|
float scale = std::min(scaleX, scaleY);
|
|
float dw = bgWidth_ * scale;
|
|
float dh = bgHeight_ * scale;
|
|
float dx = (fw - dw) / 2.0f;
|
|
float dy = (fh - dh) / 2.0f;
|
|
SDL_FRect bgDst{dx, dy, dw, dh};
|
|
SDL_RenderTexture(renderer, bgTexture_, nullptr, &bgDst);
|
|
}
|
|
|
|
// 2. Dark Overlay
|
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 160);
|
|
SDL_FRect overlayRect{0, 0, fw, fh};
|
|
SDL_RenderFillRect(renderer, &overlayRect);
|
|
|
|
// 3. Title Text
|
|
float titleY = 60.0f;
|
|
if (titleTexture_)
|
|
{
|
|
float x = (fw - titleWidth_) / 2.0f;
|
|
SDL_FRect dst{x, titleY, titleWidth_, titleHeight_};
|
|
SDL_RenderTexture(renderer, titleTexture_, nullptr, &dst);
|
|
}
|
|
|
|
// Compute bounding box for all menu items
|
|
float maxWidth = 0.0f;
|
|
for (const auto &item : items_)
|
|
{
|
|
maxWidth = std::max(maxWidth, item.width);
|
|
}
|
|
float margin = 12.0f; // 12px vertical margin rule
|
|
|
|
// Shared left edge for left-aligned items with centered bounding box
|
|
float sharedX = (fw - maxWidth) / 2.0f;
|
|
|
|
// Start menu list exactly 24 pixels below title text
|
|
float currentY = titleY + titleHeight_ + 24.0f;
|
|
|
|
for (auto &item : items_)
|
|
{
|
|
item.y = currentY;
|
|
if (item.texture)
|
|
{
|
|
SDL_FRect dst{sharedX, item.y, item.width, item.height};
|
|
SDL_RenderTexture(renderer, item.texture, nullptr, &dst);
|
|
}
|
|
currentY += item.height + margin;
|
|
}
|
|
|
|
// 6px Underline for selected item occupying half of the 12px margin space
|
|
if (selectedItem_ >= 0 && selectedItem_ < static_cast<int>(items_.size()))
|
|
{
|
|
const auto &item = items_[selectedItem_];
|
|
if (item.texture)
|
|
{
|
|
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
|
|
SDL_FRect line{sharedX, item.y + item.height + 2.0f, item.width, 6.0f};
|
|
SDL_RenderFillRect(renderer, &line);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SystemSettingsPage::createTextures(AppState &state)
|
|
{
|
|
destroyTextures();
|
|
|
|
entries_ = radiantwave::settings::systemSettingEntries();
|
|
if (selectedItem_ >= static_cast<int>(entries_.size()) + 1)
|
|
{
|
|
selectedItem_ = 0;
|
|
}
|
|
|
|
std::vector<std::string> labels;
|
|
labels.reserve(entries_.size() + 1);
|
|
for (const auto &entry : entries_)
|
|
{
|
|
labels.push_back(entry.label);
|
|
}
|
|
labels.push_back(BACK_LABEL);
|
|
|
|
// Background Image
|
|
SDL_Surface *bgSurface = IMG_Load("assets/images/RWT-Circular-Color-ForOnDark-NoText.png");
|
|
if (bgSurface)
|
|
{
|
|
bgTexture_ = SDL_CreateTextureFromSurface(state.renderer, bgSurface);
|
|
bgWidth_ = static_cast<float>(bgSurface->w);
|
|
bgHeight_ = static_cast<float>(bgSurface->h);
|
|
SDL_DestroySurface(bgSurface);
|
|
SDL_SetTextureAlphaMod(bgTexture_, 204);
|
|
}
|
|
|
|
SDL_Color white = {255, 255, 255, 255};
|
|
// Title Font
|
|
TTF_Font *fontLarge = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 64);
|
|
if (fontLarge)
|
|
{
|
|
SDL_Surface *surface = TTF_RenderText_Blended(fontLarge, TITLE, 0, white);
|
|
if (surface)
|
|
{
|
|
titleTexture_ = SDL_CreateTextureFromSurface(state.renderer, surface);
|
|
titleWidth_ = static_cast<float>(surface->w);
|
|
titleHeight_ = static_cast<float>(surface->h);
|
|
SDL_DestroySurface(surface);
|
|
}
|
|
TTF_CloseFont(fontLarge);
|
|
}
|
|
|
|
// Item Font
|
|
TTF_Font *fontItem = TTF_OpenFont("assets/fonts/Roboto-Regular.ttf", 36);
|
|
if (!fontItem)
|
|
return;
|
|
|
|
items_.reserve(labels.size());
|
|
for (const auto &label : labels)
|
|
{
|
|
MenuItem item;
|
|
item.label = label;
|
|
|
|
SDL_Surface *surface = TTF_RenderText_Blended(fontItem, label.c_str(), 0, white);
|
|
if (surface)
|
|
{
|
|
item.texture = SDL_CreateTextureFromSurface(state.renderer, surface);
|
|
item.width = static_cast<float>(surface->w);
|
|
item.height = static_cast<float>(surface->h);
|
|
SDL_DestroySurface(surface);
|
|
}
|
|
items_.push_back(std::move(item));
|
|
}
|
|
|
|
TTF_CloseFont(fontItem);
|
|
}
|
|
|
|
void SystemSettingsPage::destroyTextures()
|
|
{
|
|
if (bgTexture_)
|
|
{
|
|
SDL_DestroyTexture(bgTexture_);
|
|
bgTexture_ = nullptr;
|
|
}
|
|
if (titleTexture_)
|
|
{
|
|
SDL_DestroyTexture(titleTexture_);
|
|
titleTexture_ = nullptr;
|
|
}
|
|
for (auto &item : items_)
|
|
{
|
|
if (item.texture)
|
|
{
|
|
SDL_DestroyTexture(item.texture);
|
|
item.texture = nullptr;
|
|
}
|
|
}
|
|
items_.clear();
|
|
}
|