Compare commits
13
Commits
ec6b76618c
...
1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b0bd517c6 | ||
|
|
babb74c7ad | ||
|
|
47785cd4b9 | ||
|
|
15583edef8 | ||
|
|
63265cc22f | ||
|
|
9e73c82641 | ||
|
|
f2a40cd019 | ||
|
|
5caf1d6ef8 | ||
|
|
e17e881c00 | ||
|
|
b96631c374 | ||
|
|
39eb6d7bf7 | ||
|
|
3c9de0bee2 | ||
|
|
8e930ef8d9 |
@@ -1,19 +1,15 @@
|
||||
name: build
|
||||
|
||||
name: build-dev
|
||||
on:
|
||||
# Manual only for now. Select the branch to build when dispatching:
|
||||
# main -> APP_VERSION is the current git tag
|
||||
# dev -> APP_VERSION is the current commit hash
|
||||
workflow_dispatch:
|
||||
|
||||
# Every push to dev: build, run the offline test suite, publish to the
|
||||
# dev Attic cache.
|
||||
push:
|
||||
branches: [dev]
|
||||
jobs:
|
||||
nix-build:
|
||||
runs-on: build-server
|
||||
steps:
|
||||
# actions/checkout@v4 is unreliable on host-mode runners (its files are
|
||||
# copied into the workspace instead of the repo), so clone explicitly.
|
||||
# A full clone (no --depth) is required so `git describe --tags` on main
|
||||
# resolves the real nearest tag instead of falling back to a hash.
|
||||
- name: Checkout
|
||||
run: |
|
||||
rm -rf ./* ./.[!.]* 2>/dev/null || true
|
||||
@@ -22,7 +18,6 @@ jobs:
|
||||
"${{ gitea.server_url }}/${{ gitea.repository }}.git" .
|
||||
test -f flake.nix
|
||||
git log --oneline -1
|
||||
|
||||
- name: Inspect workspace
|
||||
run: |
|
||||
pwd
|
||||
@@ -31,28 +26,26 @@ jobs:
|
||||
git remote -v 2>&1 || true
|
||||
git describe --tags --always 2>&1 || true
|
||||
test -f flake.nix && echo "flake.nix present" || echo "NO flake.nix"
|
||||
|
||||
# Compute the version string baked into the binary's Home footer:
|
||||
# main reports the git tag, dev reports the commit hash fingerprint.
|
||||
# The key stays in the build step's environment (never echoed).
|
||||
# Dev builds fingerprint as the short commit hash.
|
||||
# Persisted via GITHUB_ENV so the impure nix eval below reads it via
|
||||
# builtins.getEnv.
|
||||
- name: Compute version
|
||||
id: version
|
||||
run: |
|
||||
if [ "${{ gitea.ref_name }}" = "main" ]; then
|
||||
APP_VERSION="$(git describe --tags --always)"
|
||||
else
|
||||
APP_VERSION="$(git rev-parse --short HEAD)"
|
||||
fi
|
||||
echo "Branch: ${{ gitea.ref_name }} -> APP_VERSION=$APP_VERSION"
|
||||
|
||||
echo "APP_VERSION=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
|
||||
# checks is keyed per-system (checks.<system>.default); nix build's
|
||||
# #attr shorthand can't resolve it, so resolve the current system inline.
|
||||
# --impure lets the flake read APP_VERSION via builtins.getEnv
|
||||
# (pure evals always get the package defaults). The offline test suite
|
||||
# runs in the package's checkPhase.
|
||||
# (pure evals always get the package defaults).
|
||||
- name: Build package
|
||||
run: |
|
||||
nix build -L ".#packages.$(nix eval --impure --raw --expr 'builtins.currentSystem').default" \
|
||||
--impure --out-link result-pkg
|
||||
# The offline test suite runs in the check derivation's checkPhase.
|
||||
# This is a separate build/output from the package itself.
|
||||
- name: Build + offline test suite
|
||||
run: nix build -L ".#checks.$(nix eval --impure --raw --expr 'builtins.currentSystem').default" --impure
|
||||
|
||||
run: |
|
||||
nix build -L ".#checks.$(nix eval --impure --raw --expr 'builtins.currentSystem').default" \
|
||||
--impure --out-link result-checks
|
||||
- name: Live portal tests
|
||||
env:
|
||||
RWT_LIVE_ACCOUNT_EMAIL: ${{ secrets.RWT_LIVE_ACCOUNT_EMAIL }}
|
||||
@@ -65,3 +58,11 @@ jobs:
|
||||
./build/test_api_client
|
||||
./build/test_license_service
|
||||
'
|
||||
# Dev closure (the actual package, not the check output) lands in the
|
||||
# dev cache.
|
||||
- name: Push to Attic dev cache
|
||||
env:
|
||||
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN_DEV }}
|
||||
run: |
|
||||
attic login radiantwave https://attic.radiantwavetech.com "$ATTIC_TOKEN"
|
||||
attic push radiantwave:radiantwave-dev ./result-pkg
|
||||
@@ -0,0 +1,76 @@
|
||||
name: build-main
|
||||
|
||||
on:
|
||||
# Manual only. Dispatch against main: build, run the offline test suite,
|
||||
# publish to the release Attic cache.
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
nix-build:
|
||||
runs-on: build-server
|
||||
steps:
|
||||
# actions/checkout@v4 is unreliable on host-mode runners (its files are
|
||||
# copied into the workspace instead of the repo), so clone explicitly.
|
||||
# A full clone (no --depth) is required so `git describe --tags` resolves
|
||||
# the real nearest tag instead of falling back to a hash.
|
||||
- name: Checkout
|
||||
run: |
|
||||
rm -rf ./* ./.[!.]* 2>/dev/null || true
|
||||
git config --global http.extraheader "Authorization: token ${{ github.token }}"
|
||||
git clone --branch "${{ gitea.ref_name }}" \
|
||||
"${{ gitea.server_url }}/${{ gitea.repository }}.git" .
|
||||
test -f flake.nix
|
||||
git log --oneline -1
|
||||
|
||||
- name: Inspect workspace
|
||||
run: |
|
||||
pwd
|
||||
ls -la
|
||||
git rev-parse --show-toplevel 2>&1 || true
|
||||
git remote -v 2>&1 || true
|
||||
git describe --tags --always 2>&1 || true
|
||||
test -f flake.nix && echo "flake.nix present" || echo "NO flake.nix"
|
||||
|
||||
# Release builds report the current git tag.
|
||||
# Persisted via GITHUB_ENV so the impure nix eval below reads it via
|
||||
# builtins.getEnv.
|
||||
- name: Compute version
|
||||
run: |
|
||||
echo "APP_VERSION=$(git describe --tags --always)" >> "$GITHUB_ENV"
|
||||
|
||||
# checks is keyed per-system (checks.<system>.default); nix build's
|
||||
# #attr shorthand can't resolve it, so resolve the current system inline.
|
||||
# --impure lets the flake read APP_VERSION via builtins.getEnv
|
||||
# (pure evals always get the package defaults).
|
||||
- name: Build package
|
||||
run: |
|
||||
nix build -L ".#packages.$(nix eval --impure --raw --expr 'builtins.currentSystem').default" \
|
||||
--impure --out-link result-pkg
|
||||
# The offline test suite runs in the check derivation's checkPhase.
|
||||
# This is a separate build/output from the package itself.
|
||||
- name: Build + offline test suite
|
||||
run: |
|
||||
nix build -L ".#checks.$(nix eval --impure --raw --expr 'builtins.currentSystem').default" \
|
||||
--impure --out-link result-checks
|
||||
|
||||
- name: Live portal tests
|
||||
env:
|
||||
RWT_LIVE_ACCOUNT_EMAIL: ${{ secrets.RWT_LIVE_ACCOUNT_EMAIL }}
|
||||
RWT_LIVE_LICENSE_EMAIL: ${{ secrets.RWT_LIVE_LICENSE_EMAIL }}
|
||||
RWT_LIVE_LICENSE_KEY: ${{ secrets.RWT_LIVE_LICENSE_KEY }}
|
||||
run: |
|
||||
nix develop --command bash -c '
|
||||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
|
||||
cmake --build build --target test_api_client test_license_service
|
||||
./build/test_api_client
|
||||
./build/test_license_service
|
||||
'
|
||||
|
||||
# Release closure (the actual package, not the check output) lands in the
|
||||
# release cache.
|
||||
- name: Push to Attic release cache
|
||||
env:
|
||||
ATTIC_TOKEN: ${{ secrets.ATTIC_TOKEN_PROD }}
|
||||
run: |
|
||||
attic login radiantwave https://attic.radiantwavetech.com "$ATTIC_TOKEN"
|
||||
attic push radiantwave:radiantwave ./result-pkg
|
||||
@@ -66,13 +66,16 @@ public:
|
||||
// Returns the current human-readable status for display (e.g. footer).
|
||||
std::string statusString() const;
|
||||
|
||||
static constexpr const char* AUDIO_BASE_URL = "https://files.radiantwavetech.com/audio/";
|
||||
static constexpr const char *AUDIO_BASE_URL =
|
||||
"https://files.radiantwavetech.com/audio/";
|
||||
static constexpr const char *MANIFEST_FILENAME = "checksums.md5";
|
||||
|
||||
private:
|
||||
void run();
|
||||
bool fetchManifest(std::vector<AudioManifestEntry> &out);
|
||||
bool downloadAndVerify(const std::string& filename, const std::string& expectedMd5, const std::string& displayName);
|
||||
bool downloadAndVerify(const std::string &filename,
|
||||
const std::string &expectedMd5,
|
||||
const std::string &displayName);
|
||||
|
||||
void setStatus(const std::string &msg);
|
||||
void wait(std::chrono::seconds seconds);
|
||||
@@ -88,7 +91,8 @@ private:
|
||||
// Parses checksums.md5 manifest text into entries. Blank lines and lines
|
||||
// starting with '#' are ignored. Lines that don't match a known format are
|
||||
// skipped. Returns false if no valid entries were found.
|
||||
bool parseManifest(const std::string& text, std::vector<AudioManifestEntry>& out);
|
||||
bool parseManifest(const std::string &text,
|
||||
std::vector<AudioManifestEntry> &out);
|
||||
|
||||
// Maps a frequency value in Hz to its internal solfeggio frequency id using
|
||||
// the same table the color palettes are seeded from (174/285->1, 396->2,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "audio_settings_page.h"
|
||||
#include "app_state.h"
|
||||
#include "logical_size.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
@@ -477,7 +478,7 @@ void AudioSettingsPage::renderModal(SDL_Renderer* renderer, float fw, float fh,
|
||||
|
||||
void AudioSettingsPage::render(SDL_Renderer* renderer) {
|
||||
int winW, winH;
|
||||
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH);
|
||||
radiantwave::getLayoutSize(renderer, &winW, &winH);
|
||||
float fw = static_cast<float>(winW);
|
||||
float fh = static_cast<float>(winH);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "date_format_page.h"
|
||||
#include "app_state.h"
|
||||
#include "date_format.h"
|
||||
#include "logical_size.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <ctime>
|
||||
@@ -131,7 +132,7 @@ void DateFormatPage::update(float dt)
|
||||
void DateFormatPage::render(SDL_Renderer* renderer)
|
||||
{
|
||||
int winW, winH;
|
||||
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH);
|
||||
radiantwave::getLayoutSize(renderer, &winW, &winH);
|
||||
float fw = static_cast<float>(winW);
|
||||
float fh = static_cast<float>(winH);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "db/db_keys.h"
|
||||
#include "provisioning.h"
|
||||
#include "validation.h"
|
||||
#include "logical_size.h"
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace {
|
||||
@@ -164,7 +165,7 @@ void EmailAddressPage::exitPage(AppState& state)
|
||||
void EmailAddressPage::render(SDL_Renderer* renderer)
|
||||
{
|
||||
int winW, winH;
|
||||
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH);
|
||||
radiantwave::getLayoutSize(renderer, &winW, &winH);
|
||||
float fw = static_cast<float>(winW);
|
||||
float fh = static_cast<float>(winH);
|
||||
|
||||
|
||||
+4
-3
@@ -8,6 +8,7 @@
|
||||
#include "audio/audio_downloader.h"
|
||||
#include "db/db_keys.h"
|
||||
#include "network/proclamation_sync.h"
|
||||
#include "logical_size.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <ctime>
|
||||
@@ -114,7 +115,7 @@ void Home::update(float dt)
|
||||
void Home::render(SDL_Renderer *renderer)
|
||||
{
|
||||
int winW, winH;
|
||||
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH);
|
||||
radiantwave::getLayoutSize(renderer, &winW, &winH);
|
||||
float fw = static_cast<float>(winW);
|
||||
float fh = static_cast<float>(winH);
|
||||
|
||||
@@ -280,10 +281,10 @@ void Home::render(SDL_Renderer *renderer)
|
||||
fourSpaces.destroy();
|
||||
|
||||
const float margin = 20.0f;
|
||||
float rectRight = fw - margin;
|
||||
float rectRight = fw - margin - 50.0f;
|
||||
float rectLeft = rectRight - (maxNameW + spaceW4 + maxValueW);
|
||||
|
||||
float lineY = fh - margin;
|
||||
float lineY = fh - margin - 50.0f;
|
||||
const float lineGap = 6.0f;
|
||||
for (size_t i = footerNames_.size(); i-- > 0;)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "license_blocked_page.h"
|
||||
#include "app_state.h"
|
||||
#include "home.h"
|
||||
#include "logical_size.h"
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
void LicenseBlockedPage::onPush(AppState& state)
|
||||
@@ -71,7 +72,7 @@ void LicenseBlockedPage::update(float dt)
|
||||
void LicenseBlockedPage::render(SDL_Renderer* renderer)
|
||||
{
|
||||
int winW, winH;
|
||||
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH);
|
||||
radiantwave::getLayoutSize(renderer, &winW, &winH);
|
||||
float fw = static_cast<float>(winW);
|
||||
float fh = static_cast<float>(winH);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "network/license_service.h"
|
||||
#include "provisioning.h"
|
||||
#include "validation.h"
|
||||
#include "logical_size.h"
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
void LicenseKeyPage::onPush(AppState& state)
|
||||
@@ -172,7 +173,7 @@ void LicenseKeyPage::exitPage(AppState& state)
|
||||
void LicenseKeyPage::render(SDL_Renderer* renderer)
|
||||
{
|
||||
int winW, winH;
|
||||
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH);
|
||||
radiantwave::getLayoutSize(renderer, &winW, &winH);
|
||||
float fw = static_cast<float>(winW);
|
||||
float fh = static_cast<float>(winH);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "log_viewer_page.h"
|
||||
#include "app_state.h"
|
||||
#include "timezone_helper.h"
|
||||
#include "logical_size.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
@@ -162,7 +163,7 @@ void LogViewerPage::update(float dt) {
|
||||
|
||||
void LogViewerPage::render(SDL_Renderer* renderer) {
|
||||
int winW, winH;
|
||||
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH);
|
||||
radiantwave::getLayoutSize(renderer, &winW, &winH);
|
||||
float fw = static_cast<float>(winW);
|
||||
float fh = static_cast<float>(winH);
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace radiantwave {
|
||||
|
||||
// Design resolution every page lays out against. The renderer's logical
|
||||
// presentation (LETTERBOX) scales this fixed space onto whatever
|
||||
// window/backbuffer the display actually provides.
|
||||
inline constexpr int kDesignWidth = 1920;
|
||||
inline constexpr int kDesignHeight = 1080;
|
||||
|
||||
// Returns the size pages should lay out against: the renderer's logical size
|
||||
// when presentation is active, otherwise the real window size. The output
|
||||
// params are always written — design constants are the last-resort fallback.
|
||||
inline void getLayoutSize(SDL_Renderer *renderer, int *w, int *h)
|
||||
{
|
||||
if (!w || !h)
|
||||
return;
|
||||
*w = kDesignWidth;
|
||||
*h = kDesignHeight;
|
||||
|
||||
if (!renderer)
|
||||
return;
|
||||
|
||||
SDL_RendererLogicalPresentation mode = SDL_LOGICAL_PRESENTATION_DISABLED;
|
||||
int logicalW = 0;
|
||||
int logicalH = 0;
|
||||
if (SDL_GetRenderLogicalPresentation(renderer, &logicalW, &logicalH,
|
||||
&mode) &&
|
||||
mode != SDL_LOGICAL_PRESENTATION_DISABLED && logicalW > 0 &&
|
||||
logicalH > 0) {
|
||||
*w = logicalW;
|
||||
*h = logicalH;
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_Window *window = SDL_GetRenderWindow(renderer);
|
||||
if (!window)
|
||||
return;
|
||||
|
||||
int windowW = 0;
|
||||
int windowH = 0;
|
||||
SDL_GetWindowSize(window, &windowW, &windowH);
|
||||
if (windowW > 0 && windowH > 0) {
|
||||
*w = windowW;
|
||||
*h = windowH;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace radiantwave
|
||||
+125
-62
@@ -4,47 +4,63 @@
|
||||
|
||||
#include "app_state.h"
|
||||
#include "home.h"
|
||||
#include "provisioning.h"
|
||||
#include "logical_size.h"
|
||||
#include "logger/logger.h"
|
||||
#include "provisioning.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
namespace {
|
||||
// How long audio fades out before the app tears down on quit. Only applied
|
||||
// when audio is actually playing; otherwise quitting is immediate.
|
||||
constexpr float kQuitFadeSeconds = 5.0f;
|
||||
} // anonymous namespace
|
||||
|
||||
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
|
||||
{
|
||||
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[]) {
|
||||
// Initialize text logger
|
||||
radiantwave::logger::Logger::instance().initialize();
|
||||
radiantwave::logger::Logger::instance().info("System", "RadiantWave Kiosk starting up");
|
||||
radiantwave::logger::Logger::instance().info(
|
||||
"System", "RadiantWave Kiosk starting up");
|
||||
|
||||
if (!SDL_Init(SDL_INIT_VIDEO))
|
||||
{
|
||||
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
||||
std::string err = SDL_GetError();
|
||||
radiantwave::logger::Logger::instance().fatal("Graphics",
|
||||
"Screen display initialization failed. Please verify graphics hardware and display connections (SDL_Init error: " + err + ")");
|
||||
radiantwave::logger::Logger::instance().fatal(
|
||||
"Graphics",
|
||||
"Screen display initialization failed. Please verify graphics "
|
||||
"hardware and display connections (SDL_Init error: " +
|
||||
err + ")");
|
||||
SDL_Log("Failed to initialize SDL: %s", err.c_str());
|
||||
return SDL_APP_FAILURE;
|
||||
}
|
||||
|
||||
auto *state = new AppState{};
|
||||
|
||||
// SDL_WINDOW_FULLSCREEN fills the desktop's native resolution, so the
|
||||
// compositor never has to scale the moving scroller (scaling moving text
|
||||
// causes flicker).
|
||||
state->window = SDL_CreateWindow("RadiantWave", 1920, 1080, SDL_WINDOW_FULLSCREEN);
|
||||
if (!state->window)
|
||||
{
|
||||
// Size the fullscreen window from the display's actual mode so the very
|
||||
// first buffer commit already matches what the compositor scanouts 1:1.
|
||||
// Requesting a hardcoded size left the backbuffer at that size on
|
||||
// displays running a different mode, and frames reached the panel
|
||||
// unscaled (quarter-size on 4K, cropped on smaller panels).
|
||||
const SDL_DisplayMode *displayMode =
|
||||
SDL_GetCurrentDisplayMode(SDL_GetPrimaryDisplay());
|
||||
int windowWidth =
|
||||
(displayMode && displayMode->w > 0) ? displayMode->w
|
||||
: radiantwave::kDesignWidth;
|
||||
int windowHeight =
|
||||
(displayMode && displayMode->h > 0) ? displayMode->h
|
||||
: radiantwave::kDesignHeight;
|
||||
|
||||
// Borderless desktop fullscreen: the compositor never has to scale the
|
||||
// moving scroller (scaling moving text causes flicker). Deliberately no
|
||||
// SDL_WINDOW_HIGH_PIXEL_DENSITY — kiosk panels are not HiDPI, so keeping
|
||||
// the pixel buffer equal to the display mode keeps scanout 1:1.
|
||||
state->window = SDL_CreateWindow("RadiantWave", windowWidth, windowHeight,
|
||||
SDL_WINDOW_FULLSCREEN);
|
||||
if (!state->window) {
|
||||
SDL_Log("Failed to create window: %s", SDL_GetError());
|
||||
delete state;
|
||||
return SDL_APP_FAILURE;
|
||||
}
|
||||
|
||||
state->renderer = SDL_CreateRenderer(state->window, nullptr);
|
||||
if (!state->renderer)
|
||||
{
|
||||
if (!state->renderer) {
|
||||
SDL_Log("Failed to create renderer: %s", SDL_GetError());
|
||||
delete state;
|
||||
return SDL_APP_FAILURE;
|
||||
@@ -53,64 +69,105 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
|
||||
// Present on the display's refresh. Without VSync the kiosk panel tears
|
||||
// fast-scrolling text into horizontal bands that flicker as the text moves.
|
||||
if (!SDL_SetRenderVSync(state->renderer, 1)) {
|
||||
radiantwave::logger::Logger::instance().warning("Graphics",
|
||||
"VSync could not be enabled; scrolling text may tear (VSync error: " + std::string(SDL_GetError()) + ")");
|
||||
radiantwave::logger::Logger::instance().warning(
|
||||
"Graphics", "VSync could not be enabled; scrolling text may tear "
|
||||
"(VSync error: " +
|
||||
std::string(SDL_GetError()) + ")");
|
||||
}
|
||||
|
||||
// Lock the renderer to a 1920x1080 logical coordinate space. All page
|
||||
// layout code already targets this resolution, so everything renders at
|
||||
// the correct size on any display — the GPU scales to native pixels.
|
||||
if (!SDL_SetRenderLogicalPresentation(state->renderer, 1920, 1080, SDL_LOGICAL_PRESENTATION_STRETCH)) {
|
||||
radiantwave::logger::Logger::instance().warning("Graphics",
|
||||
"Could not set logical render size; layout may be wrong on HiDPI displays");
|
||||
// All page layout targets the fixed design resolution; LETTERBOX maps it
|
||||
// onto the real window with aspect preserved (bars on non-16:9 panels).
|
||||
if (!SDL_SetRenderLogicalPresentation(state->renderer,
|
||||
radiantwave::kDesignWidth,
|
||||
radiantwave::kDesignHeight,
|
||||
SDL_LOGICAL_PRESENTATION_LETTERBOX)) {
|
||||
radiantwave::logger::Logger::instance().warning(
|
||||
"Graphics", "Could not set logical render size; layout may be "
|
||||
"wrong on HiDPI displays");
|
||||
}
|
||||
|
||||
{
|
||||
// Record how the requested mode, window, pixel buffer, and render
|
||||
// output actually compare — this is the evidence trail for
|
||||
// display-scaling problems on field units.
|
||||
int windowW = 0, windowH = 0;
|
||||
SDL_GetWindowSize(state->window, &windowW, &windowH);
|
||||
int pixelW = 0, pixelH = 0;
|
||||
SDL_GetWindowSizeInPixels(state->window, &pixelW, &pixelH);
|
||||
int outputW = 0, outputH = 0;
|
||||
SDL_GetRenderOutputSize(state->renderer, &outputW, &outputH);
|
||||
radiantwave::logger::Logger::instance().info(
|
||||
"Graphics",
|
||||
"Requested " + std::to_string(windowWidth) + "x" +
|
||||
std::to_string(windowHeight) + ", window " +
|
||||
std::to_string(windowW) + "x" + std::to_string(windowH) +
|
||||
", pixels " + std::to_string(pixelW) + "x" +
|
||||
std::to_string(pixelH) + ", render output " +
|
||||
std::to_string(outputW) + "x" + std::to_string(outputH) +
|
||||
", presentation LETTERBOX");
|
||||
}
|
||||
|
||||
SDL_HideCursor();
|
||||
|
||||
// Initialize configuration, database, and network manager
|
||||
if (!state->config.initialize()) {
|
||||
radiantwave::logger::Logger::instance().warning("Config",
|
||||
"Settings file could not be loaded. Default settings will be used (Config initialization failed at ~/.local/radiantwave/config.yaml)");
|
||||
radiantwave::logger::Logger::instance().warning(
|
||||
"Config", "Settings file could not be loaded. Default settings "
|
||||
"will be used (Config initialization failed at "
|
||||
"~/.local/radiantwave/config.yaml)");
|
||||
} else {
|
||||
radiantwave::logger::Logger::instance().info("Config", "Configuration initialized successfully");
|
||||
radiantwave::logger::Logger::instance().info(
|
||||
"Config", "Configuration initialized successfully");
|
||||
}
|
||||
|
||||
if (!state->db.initialize()) {
|
||||
radiantwave::logger::Logger::instance().error("Database",
|
||||
"Database storage unavailable. App features may be limited (Database initialization failed at ~/.local/radiantwave/radiantwave.db)");
|
||||
radiantwave::logger::Logger::instance().error(
|
||||
"Database", "Database storage unavailable. App features may be "
|
||||
"limited (Database initialization failed at "
|
||||
"~/.local/radiantwave/radiantwave.db)");
|
||||
} else {
|
||||
radiantwave::logger::Logger::instance().info("Database", "Database initialized and schema migrated");
|
||||
radiantwave::logger::Logger::instance().info(
|
||||
"Database", "Database initialized and schema migrated");
|
||||
}
|
||||
|
||||
if (!state->network.initialize()) {
|
||||
radiantwave::logger::Logger::instance().warning("Network",
|
||||
"WiFi network management is unavailable. Check if NetworkManager is running (D-Bus connection to org.freedesktop.NetworkManager failed)");
|
||||
radiantwave::logger::Logger::instance().warning(
|
||||
"Network", "WiFi network management is unavailable. Check if "
|
||||
"NetworkManager is running (D-Bus connection to "
|
||||
"org.freedesktop.NetworkManager failed)");
|
||||
} else {
|
||||
radiantwave::logger::Logger::instance().info("Network", "NetworkManager service connected");
|
||||
radiantwave::logger::Logger::instance().info(
|
||||
"Network", "NetworkManager service connected");
|
||||
}
|
||||
|
||||
if (!state->audio.initialize(state->config)) {
|
||||
radiantwave::logger::Logger::instance().warning("Audio",
|
||||
"Audio playback is unavailable. Check sound hardware connections (SDL3_mixer initialization failed)");
|
||||
radiantwave::logger::Logger::instance().warning(
|
||||
"Audio", "Audio playback is unavailable. Check sound hardware "
|
||||
"connections (SDL3_mixer initialization failed)");
|
||||
} else {
|
||||
radiantwave::logger::Logger::instance().info("Audio", "Audio subsystem initialized");
|
||||
radiantwave::logger::Logger::instance().info(
|
||||
"Audio", "Audio subsystem initialized");
|
||||
}
|
||||
// Let playback logs report user-facing display names from the database.
|
||||
state->audio.attachDatabase(state->db);
|
||||
|
||||
// Background download of audio assets. The worker keeps retrying until
|
||||
// internet is available, so downloads start automatically on connect.
|
||||
// The downloaded files are reconciled into the database's audio_files table.
|
||||
// The downloaded files are reconciled into the database's audio_files
|
||||
// table.
|
||||
state->audioDownloader.start(state->db);
|
||||
|
||||
// Background license validation. Home triggers a refresh whenever it is
|
||||
// displayed; the result gates access to Play and the Proclamation options.
|
||||
state->licenseTracker.start(state->db);
|
||||
|
||||
state->config.onChanged([state](const radiantwave::config::AppConfig& new_config) {
|
||||
state->config.onChanged(
|
||||
[state](const radiantwave::config::AppConfig &new_config) {
|
||||
SDL_Log("Live config update received: volume=%d, dynamic_audio=%d",
|
||||
new_config.audio.volume, new_config.audio.dynamic_audio ? 1 : 0);
|
||||
state->db.logEvent("INFO", "Config", "Live configuration hot-reloaded");
|
||||
new_config.audio.volume,
|
||||
new_config.audio.dynamic_audio ? 1 : 0);
|
||||
state->db.logEvent("INFO", "Config",
|
||||
"Live configuration hot-reloaded");
|
||||
});
|
||||
|
||||
state->keybinds.bind(SDLK_UP, "menu_up", [state]() {
|
||||
@@ -121,9 +178,9 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
|
||||
});
|
||||
|
||||
// Drive the First-Run walkthrough: shows the WiFi page when the internet
|
||||
// is unreachable, then the email and license pages when those are missing,
|
||||
// finishing on Home. The internet check is a real connectivity probe
|
||||
// (Ethernet, WiFi, or macOS all work), so it never traps a connected
|
||||
// is unreachable <DEPRECATED>, then the email and license pages when those
|
||||
// are missing, finishing on Home. The internet check is a real connectivity
|
||||
// probe (Ethernet, WiFi, or macOS all work), so it never traps a connected
|
||||
// device on the WiFi page.
|
||||
radiantwave::provisioning::begin(*state);
|
||||
|
||||
@@ -131,27 +188,36 @@ SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
|
||||
return SDL_APP_CONTINUE;
|
||||
}
|
||||
|
||||
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
|
||||
{
|
||||
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) {
|
||||
auto &state = *static_cast<AppState *>(appstate);
|
||||
|
||||
if (event->type == SDL_EVENT_QUIT)
|
||||
{
|
||||
if (event->type == SDL_EVENT_QUIT) {
|
||||
state.quitRequested = true;
|
||||
}
|
||||
|
||||
if (event->type == SDL_EVENT_WINDOW_RESIZED) {
|
||||
radiantwave::logger::Logger::instance().info(
|
||||
"Graphics", "Window resized to " +
|
||||
std::to_string(event->window.data1) + "x" +
|
||||
std::to_string(event->window.data2));
|
||||
// Logical presentation persists across resizes; re-apply defensively
|
||||
// in case backend state was reset underneath us.
|
||||
SDL_SetRenderLogicalPresentation(state.renderer,
|
||||
radiantwave::kDesignWidth,
|
||||
radiantwave::kDesignHeight,
|
||||
SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||
}
|
||||
|
||||
if (!state.keybinds.handleEvent(*event)) {
|
||||
state.pages.handleEvent(*event);
|
||||
}
|
||||
|
||||
if (state.quitRequested && state.quitFadeRemaining <= 0.0f)
|
||||
{
|
||||
if (state.quitRequested && state.quitFadeRemaining <= 0.0f) {
|
||||
// Graceful shutdown: start the audio fade-out and keep the loop
|
||||
// running until it completes, then let SDL_AppQuit tear everything
|
||||
// down. When nothing is playing there is nothing to fade and quitting
|
||||
// is immediate.
|
||||
if (state.audio.stopTrackFadeOut(kQuitFadeSeconds))
|
||||
{
|
||||
if (state.audio.stopTrackFadeOut(kQuitFadeSeconds)) {
|
||||
state.quitFadeRemaining = kQuitFadeSeconds;
|
||||
return SDL_APP_CONTINUE;
|
||||
}
|
||||
@@ -161,23 +227,21 @@ SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
|
||||
return SDL_APP_CONTINUE;
|
||||
}
|
||||
|
||||
SDL_AppResult SDL_AppIterate(void *appstate)
|
||||
{
|
||||
SDL_AppResult SDL_AppIterate(void *appstate) {
|
||||
auto &state = *static_cast<AppState *>(appstate);
|
||||
|
||||
Uint64 nowTicks = SDL_GetTicks();
|
||||
static Uint64 lastTicks = nowTicks;
|
||||
float dt = static_cast<float>(nowTicks - lastTicks) / 1000.0f;
|
||||
if (dt > 0.1f) dt = 0.1f;
|
||||
if (dt > 0.1f)
|
||||
dt = 0.1f;
|
||||
lastTicks = nowTicks;
|
||||
|
||||
state.pages.update(dt);
|
||||
|
||||
if (state.quitFadeRemaining > 0.0f)
|
||||
{
|
||||
if (state.quitFadeRemaining > 0.0f) {
|
||||
state.quitFadeRemaining -= dt;
|
||||
if (state.quitFadeRemaining <= 0.0f)
|
||||
{
|
||||
if (state.quitFadeRemaining <= 0.0f) {
|
||||
return SDL_APP_SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -191,8 +255,7 @@ SDL_AppResult SDL_AppIterate(void *appstate)
|
||||
return SDL_APP_CONTINUE;
|
||||
}
|
||||
|
||||
void SDL_AppQuit(void *appstate, SDL_AppResult result)
|
||||
{
|
||||
void SDL_AppQuit(void *appstate, SDL_AppResult result) {
|
||||
auto *state = static_cast<AppState *>(appstate);
|
||||
if (state) {
|
||||
state->audioDownloader.shutdown();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "proclamation_options_page.h"
|
||||
#include "app_state.h"
|
||||
#include "network/proclamation_sync.h"
|
||||
#include "logical_size.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
@@ -197,7 +198,7 @@ void ProclamationOptionsPage::rebuildItems()
|
||||
FlatItem::Type::SubCategory,
|
||||
std::to_string(sub.id),
|
||||
sub.name,
|
||||
1, false, false, false,
|
||||
1, true, false, false,
|
||||
});
|
||||
|
||||
for (const auto& p : proclamations_) {
|
||||
@@ -267,7 +268,7 @@ void ProclamationOptionsPage::rebuildVisible()
|
||||
parentVisible = true; // the always-on top-level proclamation
|
||||
} else if (currentRoot >= 0 && items_[currentRoot].expanded) {
|
||||
parentVisible = (item.depth == 1) ||
|
||||
(item.depth == 2 && currentSub >= 0 && items_[currentSub].expanded);
|
||||
(item.depth == 2 && currentSub >= 0);
|
||||
}
|
||||
if (parentVisible) visible_.push_back(i);
|
||||
}
|
||||
@@ -282,7 +283,7 @@ void ProclamationOptionsPage::toggleItem()
|
||||
if (visible_.empty()) return;
|
||||
FlatItem& item = items_[visible_[highlightIndex_]];
|
||||
|
||||
if (item.type == FlatItem::Type::RootCategory || item.type == FlatItem::Type::SubCategory) {
|
||||
if (item.type == FlatItem::Type::RootCategory) {
|
||||
item.expanded = !item.expanded;
|
||||
rebuildVisible();
|
||||
return;
|
||||
@@ -387,7 +388,7 @@ void ProclamationOptionsPage::collapseCurrent()
|
||||
{
|
||||
if (visible_.empty()) return;
|
||||
FlatItem& item = items_[visible_[highlightIndex_]];
|
||||
if (item.type == FlatItem::Type::RootCategory || item.type == FlatItem::Type::SubCategory) {
|
||||
if (item.type == FlatItem::Type::RootCategory) {
|
||||
if (item.expanded) {
|
||||
item.expanded = false;
|
||||
rebuildVisible();
|
||||
@@ -399,7 +400,7 @@ void ProclamationOptionsPage::expandCurrent()
|
||||
{
|
||||
if (visible_.empty()) return;
|
||||
FlatItem& item = items_[visible_[highlightIndex_]];
|
||||
if (item.type == FlatItem::Type::RootCategory || item.type == FlatItem::Type::SubCategory) {
|
||||
if (item.type == FlatItem::Type::RootCategory) {
|
||||
if (!item.expanded) {
|
||||
item.expanded = true;
|
||||
rebuildVisible();
|
||||
@@ -525,8 +526,7 @@ void ProclamationOptionsPage::handleEvent(const SDL_Event& event)
|
||||
if (searchActive_) return;
|
||||
if (!visible_.empty()) {
|
||||
const FlatItem& item = items_[visible_[highlightIndex_]];
|
||||
if ((item.type == FlatItem::Type::RootCategory ||
|
||||
item.type == FlatItem::Type::SubCategory) && item.expanded) {
|
||||
if (item.type == FlatItem::Type::RootCategory && item.expanded) {
|
||||
collapseCurrent();
|
||||
} else {
|
||||
moveToParent();
|
||||
@@ -552,7 +552,7 @@ void ProclamationOptionsPage::update(float dt)
|
||||
void ProclamationOptionsPage::render(SDL_Renderer* renderer)
|
||||
{
|
||||
int winW, winH;
|
||||
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH);
|
||||
radiantwave::getLayoutSize(renderer, &winW, &winH);
|
||||
float fw = static_cast<float>(winW);
|
||||
float fh = static_cast<float>(winH);
|
||||
|
||||
@@ -654,14 +654,18 @@ void ProclamationOptionsPage::render(SDL_Renderer* renderer)
|
||||
SDL_RenderTexture(renderer, emptyHintText_.texture, nullptr, &dst);
|
||||
}
|
||||
} else {
|
||||
// Compute the widest row so the block can be centered.
|
||||
// Stable width: compute from every result so the list doesn't shift.
|
||||
float maxTotalWidth = 0.0f;
|
||||
for (const auto& r : searchResults_) {
|
||||
int w = 0, h = 0;
|
||||
TTF_GetStringSize(fontItem_, proclamations_[r].title.c_str(), 0, &w, &h);
|
||||
maxTotalWidth = std::max(maxTotalWidth, static_cast<float>(w));
|
||||
}
|
||||
|
||||
std::vector<float> textWidths(static_cast<size_t>(listSize));
|
||||
for (int i = scrollOffset_; i < scrollOffset_ + visibleCount_ && i < listSize; ++i) {
|
||||
const std::string& label = proclamations_[searchResults_[i]].title;
|
||||
int w = 0, h = 0;
|
||||
TTF_GetStringSize(fontItem_, label.c_str(), 0, &w, &h);
|
||||
maxTotalWidth = std::max(maxTotalWidth, static_cast<float>(w));
|
||||
TTF_GetStringSize(fontItem_, proclamations_[searchResults_[i]].title.c_str(), 0, &w, &h);
|
||||
textWidths[i] = static_cast<float>(w);
|
||||
}
|
||||
float sharedX = (fw - maxTotalWidth) / 2.0f;
|
||||
@@ -694,19 +698,29 @@ void ProclamationOptionsPage::render(SDL_Renderer* renderer)
|
||||
}
|
||||
} else {
|
||||
// --- Browse tree ---
|
||||
// Compute the widest row so the block can be centered.
|
||||
// Stable width: compute from every item so the list doesn't
|
||||
// shift left/right when groups expand or collapse.
|
||||
float maxTotalWidth = 0.0f;
|
||||
std::vector<float> textWidths(visible_.size());
|
||||
for (int i = scrollOffset_; i < scrollOffset_ + visibleCount_ && i < static_cast<int>(visible_.size()); ++i) {
|
||||
const FlatItem& item = items_[visible_[i]];
|
||||
for (const auto& item : items_) {
|
||||
std::string label = item.label;
|
||||
if (item.type == FlatItem::Type::RootCategory || item.type == FlatItem::Type::SubCategory) {
|
||||
if (item.type == FlatItem::Type::RootCategory) {
|
||||
label = (item.expanded ? "[-] " : "[+] ") + label;
|
||||
}
|
||||
int w = 0, h = 0;
|
||||
TTF_GetStringSize(fontItem_, label.c_str(), 0, &w, &h);
|
||||
float total = item.depth * indent + static_cast<float>(w);
|
||||
maxTotalWidth = std::max(maxTotalWidth, total);
|
||||
}
|
||||
|
||||
std::vector<float> textWidths(visible_.size());
|
||||
for (int i = scrollOffset_; i < scrollOffset_ + visibleCount_ && i < static_cast<int>(visible_.size()); ++i) {
|
||||
const FlatItem& item = items_[visible_[i]];
|
||||
std::string label = item.label;
|
||||
if (item.type == FlatItem::Type::RootCategory) {
|
||||
label = (item.expanded ? "[-] " : "[+] ") + label;
|
||||
}
|
||||
int w = 0, h = 0;
|
||||
TTF_GetStringSize(fontItem_, label.c_str(), 0, &w, &h);
|
||||
textWidths[i] = static_cast<float>(w);
|
||||
}
|
||||
|
||||
@@ -720,7 +734,7 @@ void ProclamationOptionsPage::render(SDL_Renderer* renderer)
|
||||
float y = listTop + rowIndex * rowPitch;
|
||||
|
||||
std::string label = item.label;
|
||||
if (item.type == FlatItem::Type::RootCategory || item.type == FlatItem::Type::SubCategory) {
|
||||
if (item.type == FlatItem::Type::RootCategory) {
|
||||
label = (item.expanded ? "[-] " : "[+] ") + label;
|
||||
}
|
||||
|
||||
|
||||
+18
-16
@@ -1,19 +1,18 @@
|
||||
#include "provisioning.h"
|
||||
#include "wifi_setup_page.h"
|
||||
#include "email_address_page.h"
|
||||
#include "license_key_page.h"
|
||||
#include "home.h"
|
||||
#include "license_key_page.h"
|
||||
#include "logger/logger.h"
|
||||
#include "wifi_setup_page.h"
|
||||
|
||||
namespace radiantwave::provisioning {
|
||||
|
||||
// Push the page for a walkthrough step, or Home when the walkthrough is done.
|
||||
static void pushStep(AppState& state, Step step)
|
||||
{
|
||||
static void pushStep(AppState &state, Step step) {
|
||||
switch (step) {
|
||||
case Step::Internet:
|
||||
state.pages.push(state, std::make_unique<WiFiSetupPage>());
|
||||
break;
|
||||
// case Step::Internet:
|
||||
// state.pages.push(state, std::make_unique<WiFiSetupPage>());
|
||||
// break;
|
||||
case Step::Email:
|
||||
state.pages.push(state, std::make_unique<EmailAddressPage>());
|
||||
break;
|
||||
@@ -27,8 +26,7 @@ static void pushStep(AppState& state, Step step)
|
||||
}
|
||||
}
|
||||
|
||||
void begin(AppState& state)
|
||||
{
|
||||
void begin(AppState &state) {
|
||||
// Without the database there is nowhere to persist email/license, so the
|
||||
// walkthrough would trap the user on pages that can never be completed.
|
||||
if (!state.db.isOpen()) {
|
||||
@@ -38,14 +36,17 @@ void begin(AppState& state)
|
||||
}
|
||||
|
||||
const bool internetUp = state.network.hasInternet();
|
||||
const bool hasEmail = !state.db.getKV(radiantwave::db::KV_EMAIL_ADDRESS).empty();
|
||||
const bool hasLicense = !state.db.getKV(radiantwave::db::KV_LICENSE_KEY).empty();
|
||||
const bool hasEmail =
|
||||
!state.db.getKV(radiantwave::db::KV_EMAIL_ADDRESS).empty();
|
||||
const bool hasLicense =
|
||||
!state.db.getKV(radiantwave::db::KV_LICENSE_KEY).empty();
|
||||
|
||||
if (indicatesFirstRun(internetUp, hasEmail, hasLicense)) {
|
||||
// Placeholder: a brand-new device. Later this flag will launch the
|
||||
// first-run welcome walkthrough (TOS, quick user guide, ...).
|
||||
state.firstRunWalkthrough = true;
|
||||
radiantwave::logger::Logger::instance().info("Provisioning",
|
||||
radiantwave::logger::Logger::instance().info(
|
||||
"Provisioning",
|
||||
"First-Run detected (no internet, no email, no license); "
|
||||
"first-run welcome walkthrough is not yet implemented");
|
||||
}
|
||||
@@ -55,8 +56,7 @@ void begin(AppState& state)
|
||||
pushStep(state, firstMissingStep(internetUp, hasEmail, hasLicense));
|
||||
}
|
||||
|
||||
void advance(AppState& state)
|
||||
{
|
||||
void advance(AppState &state) {
|
||||
if (state.pages.depth() > 0) {
|
||||
state.pages.pop(state);
|
||||
}
|
||||
@@ -70,8 +70,10 @@ void advance(AppState& state)
|
||||
state.walkthroughInternetUp = internetUp;
|
||||
}
|
||||
|
||||
const bool hasEmail = !state.db.getKV(radiantwave::db::KV_EMAIL_ADDRESS).empty();
|
||||
const bool hasLicense = !state.db.getKV(radiantwave::db::KV_LICENSE_KEY).empty();
|
||||
const bool hasEmail =
|
||||
!state.db.getKV(radiantwave::db::KV_EMAIL_ADDRESS).empty();
|
||||
const bool hasLicense =
|
||||
!state.db.getKV(radiantwave::db::KV_LICENSE_KEY).empty();
|
||||
|
||||
state.provisioningMode = true;
|
||||
pushStep(state, firstMissingStep(internetUp, hasEmail, hasLicense));
|
||||
|
||||
+8
-8
@@ -24,21 +24,21 @@
|
||||
namespace radiantwave::provisioning {
|
||||
|
||||
// Prerequisite steps in walkthrough priority order.
|
||||
enum class Step { Done, Internet, Email, License };
|
||||
enum class Step { Done, Email, License };
|
||||
|
||||
// First unsatisfied step, given the current conditions.
|
||||
inline Step firstMissingStep(bool internetUp, bool hasEmail, bool hasLicense)
|
||||
{
|
||||
if (!internetUp) return Step::Internet;
|
||||
if (!hasEmail) return Step::Email;
|
||||
if (!hasLicense) return Step::License;
|
||||
inline Step firstMissingStep(bool internetUp, bool hasEmail, bool hasLicense) {
|
||||
// if (!internetUp) return Step::Internet;
|
||||
if (!hasEmail)
|
||||
return Step::Email;
|
||||
if (!hasLicense)
|
||||
return Step::License;
|
||||
return Step::Done;
|
||||
}
|
||||
|
||||
// True when nothing is configured (no internet, no email, no license).
|
||||
// Signals a brand-new, never-provisioned device.
|
||||
inline bool indicatesFirstRun(bool internetUp, bool hasEmail, bool hasLicense)
|
||||
{
|
||||
inline bool indicatesFirstRun(bool internetUp, bool hasEmail, bool hasLicense) {
|
||||
return !internetUp && !hasEmail && !hasLicense;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "proclamation_options_page.h"
|
||||
#include "scramble/fibonacci_scrambler.h"
|
||||
#include "utf8_utils.h"
|
||||
#include "logical_size.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_ttf/SDL_ttf.h>
|
||||
@@ -35,7 +36,7 @@ void ScrollerPage::onPush(AppState &state)
|
||||
|
||||
int winW = 0;
|
||||
int winH = 0;
|
||||
SDL_GetWindowSize(state.window, &winW, &winH);
|
||||
radiantwave::getLayoutSize(state.renderer, &winW, &winH);
|
||||
screenW_ = static_cast<float>(winW);
|
||||
screenH_ = static_cast<float>(winH);
|
||||
// Lines are capped at the full screen width; the widest Fibonacci lines are
|
||||
|
||||
+20
-19
@@ -10,6 +10,7 @@
|
||||
#include "email_address_page.h"
|
||||
#include "license_key_page.h"
|
||||
#include "date_format_page.h"
|
||||
#include "logical_size.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
#include <algorithm>
|
||||
@@ -18,18 +19,18 @@ namespace
|
||||
{
|
||||
|
||||
constexpr int MENU_ITEM_COUNT = 9;
|
||||
// 0 = WiFi Setup, 1 = Audio Options, 2 = Time Zone Setup, 3 = Proclamations,
|
||||
// 4 = Email Address, 5 = License Key, 6 = Date Format,
|
||||
// 7 = View System Logs, 8 = Back
|
||||
// 0 = Proclamations, 1 = Audio Options, 2 = Email Address, 3 = License Key,
|
||||
// 4 = WiFi Setup, 5 = Time Zone Setup, 6 = Date Format,
|
||||
// 7 = System Logs, 8 = Back
|
||||
constexpr const char *MENU_LABELS[MENU_ITEM_COUNT] = {
|
||||
"WiFi Setup",
|
||||
"Audio Options",
|
||||
"Time Zone Setup",
|
||||
"Proclamations",
|
||||
"Audio Options",
|
||||
"Email Address",
|
||||
"License Key",
|
||||
"WiFi Setup",
|
||||
"Time Zone Setup",
|
||||
"Date Format",
|
||||
"View System Logs",
|
||||
"System Logs",
|
||||
"Back to Home",
|
||||
};
|
||||
|
||||
@@ -77,15 +78,6 @@ void Settings::handleEvent(const SDL_Event &event)
|
||||
switch (selectedItem_)
|
||||
{
|
||||
case 0:
|
||||
state_->pages.push(*state_, std::make_unique<WiFiSetupPage>());
|
||||
break;
|
||||
case 1:
|
||||
state_->pages.push(*state_, std::make_unique<AudioSettingsPage>());
|
||||
break;
|
||||
case 2:
|
||||
state_->pages.push(*state_, std::make_unique<TimeZonePage>());
|
||||
break;
|
||||
case 3:
|
||||
// The Proclamation list is license-gated like Play.
|
||||
if (state_->licenseTracker.isValid())
|
||||
{
|
||||
@@ -96,12 +88,21 @@ void Settings::handleEvent(const SDL_Event &event)
|
||||
state_->pages.push(*state_, std::make_unique<LicenseBlockedPage>());
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
case 1:
|
||||
state_->pages.push(*state_, std::make_unique<AudioSettingsPage>());
|
||||
break;
|
||||
case 2:
|
||||
state_->pages.push(*state_, std::make_unique<EmailAddressPage>());
|
||||
break;
|
||||
case 5:
|
||||
case 3:
|
||||
state_->pages.push(*state_, std::make_unique<LicenseKeyPage>());
|
||||
break;
|
||||
case 4:
|
||||
state_->pages.push(*state_, std::make_unique<WiFiSetupPage>());
|
||||
break;
|
||||
case 5:
|
||||
state_->pages.push(*state_, std::make_unique<TimeZonePage>());
|
||||
break;
|
||||
case 6:
|
||||
state_->pages.push(*state_, std::make_unique<DateFormatPage>());
|
||||
break;
|
||||
@@ -128,7 +129,7 @@ void Settings::update(float dt)
|
||||
void Settings::render(SDL_Renderer *renderer)
|
||||
{
|
||||
int winW, winH;
|
||||
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH);
|
||||
radiantwave::getLayoutSize(renderer, &winW, &winH);
|
||||
float fw = static_cast<float>(winW);
|
||||
float fh = static_cast<float>(winH);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "timezone_page.h"
|
||||
#include "app_state.h"
|
||||
#include "logical_size.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <ctime>
|
||||
@@ -199,7 +200,7 @@ void TimeZonePage::update(float dt) {
|
||||
|
||||
void TimeZonePage::render(SDL_Renderer* renderer) {
|
||||
int winW, winH;
|
||||
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH);
|
||||
radiantwave::getLayoutSize(renderer, &winW, &winH);
|
||||
float fw = static_cast<float>(winW);
|
||||
float fh = static_cast<float>(winH);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "wifi_setup_page.h"
|
||||
#include "app_state.h"
|
||||
#include "provisioning.h"
|
||||
#include "logical_size.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
@@ -301,7 +302,7 @@ void WiFiSetupPage::update(float dt) {
|
||||
|
||||
void WiFiSetupPage::render(SDL_Renderer* renderer) {
|
||||
int winW, winH;
|
||||
SDL_GetWindowSize(SDL_GetRenderWindow(renderer), &winW, &winH);
|
||||
radiantwave::getLayoutSize(renderer, &winW, &winH);
|
||||
float fw = static_cast<float>(winW);
|
||||
float fh = static_cast<float>(winH);
|
||||
|
||||
|
||||
@@ -1,50 +1,36 @@
|
||||
#include "test_harness.h"
|
||||
#include "../src/provisioning.h"
|
||||
#include "test_harness.h"
|
||||
|
||||
using radiantwave::provisioning::firstMissingStep;
|
||||
using radiantwave::provisioning::indicatesFirstRun;
|
||||
using radiantwave::provisioning::Step;
|
||||
|
||||
TEST(first_missing_step_all_satisfied)
|
||||
{
|
||||
TEST(first_missing_step_all_satisfied) {
|
||||
ASSERT_EQ(firstMissingStep(true, true, true), Step::Done);
|
||||
}
|
||||
|
||||
TEST(first_missing_step_no_internet)
|
||||
{
|
||||
// Internet is checked before email/license regardless of the others.
|
||||
ASSERT_EQ(firstMissingStep(false, false, false), Step::Internet);
|
||||
ASSERT_EQ(firstMissingStep(false, true, true), Step::Internet);
|
||||
}
|
||||
|
||||
TEST(first_missing_step_no_email)
|
||||
{
|
||||
TEST(first_missing_step_no_email) {
|
||||
ASSERT_EQ(firstMissingStep(true, false, false), Step::Email);
|
||||
ASSERT_EQ(firstMissingStep(true, false, true), Step::Email);
|
||||
}
|
||||
|
||||
TEST(first_missing_step_no_license)
|
||||
{
|
||||
TEST(first_missing_step_no_license) {
|
||||
ASSERT_EQ(firstMissingStep(true, true, false), Step::License);
|
||||
}
|
||||
|
||||
TEST(first_run_all_missing)
|
||||
{
|
||||
TEST(first_run_all_missing) {
|
||||
ASSERT_TRUE(indicatesFirstRun(false, false, false));
|
||||
}
|
||||
|
||||
TEST(first_run_has_internet)
|
||||
{
|
||||
TEST(first_run_has_internet) {
|
||||
ASSERT_FALSE(indicatesFirstRun(true, false, false));
|
||||
}
|
||||
|
||||
TEST(first_run_has_email)
|
||||
{
|
||||
TEST(first_run_has_email) {
|
||||
ASSERT_FALSE(indicatesFirstRun(false, true, false));
|
||||
}
|
||||
|
||||
TEST(first_run_has_license)
|
||||
{
|
||||
TEST(first_run_has_license) {
|
||||
ASSERT_FALSE(indicatesFirstRun(false, false, true));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user