Warning screen - New reusable WarningPage (word-wrapped message, ENTER acknowledges and continues to the screen you were about to enter). - First use: pressing Play while the selected audio is still downloading. Dynamic Audio requires every file; a fixed track requires its own file. License re-validation - No longer re-verifies on every Home render. Checks now run only when the email/license key is entered or changed, on the manual option, at startup when already due, and hourly while valid. - Invalid + unchanged credentials are never re-checked (persisted pair). - Valid re-checks at most once per hour, anchored on a DB timestamp so an overdue check runs immediately after a power cycle. - New system_kv keys track the checked pair, outcome, and last-check time. - Added a System Settings row "Manually Validate License Key OK|Invalid" (OK green, Invalid red, label white) that runs a check immediately. Portal / local integration - License validation request now sends the app version as `appVersion`. - The entered license key is mirrored to ~/.local/radiantwave/license.txt. Misc - "Please connect to WiFi" only shows when no WiFi is joined AND the internet is unreachable (fixes the NetworkManager-missing and Ethernet cases). - Added unit tests for the audio-download warning, license scheduling policy, license request shape, and the new settings-menu entry.
244 lines
7.1 KiB
C++
244 lines
7.1 KiB
C++
#include "email_address_page.h"
|
|
#include "app_state.h"
|
|
#include "db/db_keys.h"
|
|
#include "provisioning.h"
|
|
#include "validation.h"
|
|
#include "logical_size.h"
|
|
#include <SDL3/SDL.h>
|
|
|
|
namespace {
|
|
|
|
constexpr size_t MAX_EMAIL_LENGTH = 254;
|
|
|
|
} // anonymous namespace
|
|
|
|
void EmailAddressPage::onPush(AppState& state)
|
|
{
|
|
state_ = &state;
|
|
TTF_Init();
|
|
createFonts();
|
|
|
|
storedValue_ = state.db.getKV(radiantwave::db::KV_EMAIL_ADDRESS);
|
|
input_ = storedValue_;
|
|
statusMessage_ = "Enter or edit the email address, then press ENTER";
|
|
|
|
if (state.window) {
|
|
SDL_StartTextInput(state.window);
|
|
}
|
|
}
|
|
|
|
void EmailAddressPage::onResume(AppState& state)
|
|
{
|
|
state_ = &state;
|
|
createFonts();
|
|
storedValue_ = state.db.getKV(radiantwave::db::KV_EMAIL_ADDRESS);
|
|
input_ = storedValue_;
|
|
|
|
if (state.window) {
|
|
SDL_StartTextInput(state.window);
|
|
}
|
|
}
|
|
|
|
void EmailAddressPage::onPause(AppState& state)
|
|
{
|
|
state.keybinds.unbindAll();
|
|
if (state.window) {
|
|
SDL_StopTextInput(state.window);
|
|
}
|
|
destroyFonts();
|
|
destroyTextures();
|
|
}
|
|
|
|
void EmailAddressPage::onPop(AppState& state)
|
|
{
|
|
if (state.window) {
|
|
SDL_StopTextInput(state.window);
|
|
}
|
|
destroyFonts();
|
|
destroyTextures();
|
|
TTF_Quit();
|
|
}
|
|
|
|
void EmailAddressPage::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 EmailAddressPage::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 EmailAddressPage::destroyTextures()
|
|
{
|
|
titleText_.destroy();
|
|
storedText_.destroy();
|
|
valueText_.destroy();
|
|
statusText_.destroy();
|
|
hintText_.destroy();
|
|
}
|
|
|
|
void EmailAddressPage::submit()
|
|
{
|
|
if (input_.empty()) {
|
|
if (state_->db.deleteKV(radiantwave::db::KV_EMAIL_ADDRESS)) {
|
|
storedValue_.clear();
|
|
statusMessage_ = "Cleared";
|
|
returnTimer_ = 1.0f;
|
|
// The credential pair changed: re-validate in the background.
|
|
state_->licenseTracker.refresh();
|
|
} else {
|
|
statusMessage_ = "Failed to clear email address";
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!radiantwave::validation::isValidEmail(input_)) {
|
|
statusMessage_ = "Invalid email address";
|
|
return;
|
|
}
|
|
|
|
if (state_->db.setKV(radiantwave::db::KV_EMAIL_ADDRESS, input_)) {
|
|
storedValue_ = input_;
|
|
statusMessage_ = "Saved";
|
|
returnTimer_ = 1.0f;
|
|
// The credential pair changed: re-validate in the background.
|
|
state_->licenseTracker.refresh();
|
|
} else {
|
|
statusMessage_ = "Failed to save email address";
|
|
}
|
|
}
|
|
|
|
void EmailAddressPage::handleEvent(const SDL_Event& event)
|
|
{
|
|
if (!state_) return;
|
|
|
|
if (event.type == SDL_EVENT_TEXT_INPUT) {
|
|
if (input_.size() < MAX_EMAIL_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) {
|
|
exitPage(*state_);
|
|
}
|
|
}
|
|
}
|
|
|
|
void EmailAddressPage::update(float dt)
|
|
{
|
|
if (returnTimer_ > 0.0f) {
|
|
returnTimer_ -= dt;
|
|
if (returnTimer_ <= 0.0f) {
|
|
returnTimer_ = -1.0f;
|
|
if (state_) {
|
|
exitPage(*state_);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// In the First-Run walkthrough a finished step moves on to the next missing
|
|
// prerequisite; otherwise the page returns to whatever pushed it (Settings).
|
|
void EmailAddressPage::exitPage(AppState& state)
|
|
{
|
|
if (state.provisioningMode) {
|
|
radiantwave::provisioning::advance(state);
|
|
} else {
|
|
state.pages.pop(state);
|
|
}
|
|
}
|
|
|
|
void EmailAddressPage::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};
|
|
|
|
float currentY = 30.0f;
|
|
|
|
// Input box (centered on screen); messages align to its left edge
|
|
float boxW = 700.0f;
|
|
float boxH = 70.0f;
|
|
float boxX = (fw - boxW) / 2.0f;
|
|
|
|
titleText_.ensure(renderer, fontTitle_, "Email Address", 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;
|
|
|
|
// Status message (left-aligned with input box)
|
|
if (!statusMessage_.empty()) {
|
|
statusText_.ensure(renderer, fontHeader_, statusMessage_, 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_,
|
|
"[Type] Enter email [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);
|
|
}
|
|
}
|