Remove subscription levels; license valid = subscribed
This commit is contained in:
+12
-14
@@ -132,7 +132,6 @@ bool DatabaseManager::runMigrations() {
|
||||
selected INTEGER DEFAULT 0,
|
||||
category_id INTEGER DEFAULT 0,
|
||||
solfeggio_frequency_id INTEGER DEFAULT 0,
|
||||
required_level_id INTEGER DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
@@ -184,8 +183,11 @@ bool DatabaseManager::runMigrations() {
|
||||
ensureColumn("proclamations", "selected", "selected INTEGER DEFAULT 0");
|
||||
ensureColumn("proclamations", "category_id", "category_id INTEGER DEFAULT 0");
|
||||
ensureColumn("proclamations", "solfeggio_frequency_id", "solfeggio_frequency_id INTEGER DEFAULT 0");
|
||||
ensureColumn("proclamations", "required_level_id", "required_level_id INTEGER DEFAULT 1");
|
||||
ensureColumn("audio_files", "solfeggio_frequency_id", "solfeggio_frequency_id INTEGER DEFAULT 0");
|
||||
// Clean up the pre-active/inactive-license subscription level keys.
|
||||
// Existing devices may still carry them; nothing reads them anymore.
|
||||
executeSql("DELETE FROM system_kv WHERE key IN "
|
||||
"('subscription_level_id','subscription_level_name','subscription_level_description');");
|
||||
}
|
||||
if (ok) {
|
||||
seedColorPalettes();
|
||||
@@ -336,7 +338,6 @@ void DatabaseManager::bindProclamationRecord(sqlite3_stmt* stmt, const Proclamat
|
||||
sqlite3_bind_int(stmt, 7, record.selected ? 1 : 0);
|
||||
sqlite3_bind_int(stmt, 8, record.category_id);
|
||||
sqlite3_bind_int(stmt, 9, record.solfeggio_frequency_id);
|
||||
sqlite3_bind_int(stmt, 10, record.required_level_id);
|
||||
}
|
||||
|
||||
bool DatabaseManager::saveProclamation(const ProclamationRecord& record) {
|
||||
@@ -344,13 +345,12 @@ bool DatabaseManager::saveProclamation(const ProclamationRecord& record) {
|
||||
if (!db_) return false;
|
||||
|
||||
const char* sql = "INSERT INTO proclamations (id, title, content, solfeggio_freq_hz, category, "
|
||||
"is_active, selected, category_id, solfeggio_frequency_id, required_level_id) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
|
||||
"is_active, selected, category_id, solfeggio_frequency_id) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) "
|
||||
"ON CONFLICT(id) DO UPDATE SET title=excluded.title, content=excluded.content, "
|
||||
"solfeggio_freq_hz=excluded.solfeggio_freq_hz, category=excluded.category, "
|
||||
"is_active=excluded.is_active, selected=excluded.selected, "
|
||||
"category_id=excluded.category_id, solfeggio_frequency_id=excluded.solfeggio_frequency_id, "
|
||||
"required_level_id=excluded.required_level_id;";
|
||||
"category_id=excluded.category_id, solfeggio_frequency_id=excluded.solfeggio_frequency_id;";
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
@@ -375,7 +375,6 @@ static ProclamationRecord readProclamationRecord(sqlite3_stmt* stmt, int firstCo
|
||||
rec.selected = (sqlite3_column_int(stmt, firstCol + 6) != 0);
|
||||
rec.category_id = sqlite3_column_int(stmt, firstCol + 7);
|
||||
rec.solfeggio_frequency_id = sqlite3_column_int(stmt, firstCol + 8);
|
||||
rec.required_level_id = sqlite3_column_int(stmt, firstCol + 9);
|
||||
return rec;
|
||||
}
|
||||
|
||||
@@ -385,7 +384,7 @@ std::vector<ProclamationRecord> DatabaseManager::getActiveProclamations() {
|
||||
if (!db_) return result;
|
||||
|
||||
const char* sql = "SELECT id, title, content, solfeggio_freq_hz, category, is_active, "
|
||||
"selected, category_id, solfeggio_frequency_id, required_level_id "
|
||||
"selected, category_id, solfeggio_frequency_id "
|
||||
"FROM proclamations WHERE is_active = 1;";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
@@ -500,13 +499,12 @@ bool DatabaseManager::syncProclamations(const std::vector<ProclamationRecord>& r
|
||||
// Upsert each proclamation, preserving the stashed selection state.
|
||||
if (ok) {
|
||||
const char* sql = "INSERT INTO proclamations (id, title, content, solfeggio_freq_hz, category, "
|
||||
"is_active, selected, category_id, solfeggio_frequency_id, required_level_id) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
|
||||
"is_active, selected, category_id, solfeggio_frequency_id) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) "
|
||||
"ON CONFLICT(id) DO UPDATE SET title=excluded.title, content=excluded.content, "
|
||||
"solfeggio_freq_hz=excluded.solfeggio_freq_hz, category=excluded.category, "
|
||||
"is_active=excluded.is_active, category_id=excluded.category_id, "
|
||||
"solfeggio_frequency_id=excluded.solfeggio_frequency_id, "
|
||||
"required_level_id=excluded.required_level_id;";
|
||||
"solfeggio_frequency_id=excluded.solfeggio_frequency_id;";
|
||||
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
@@ -557,7 +555,7 @@ std::vector<ProclamationRecord> DatabaseManager::getAllProclamations() {
|
||||
if (!db_) return result;
|
||||
|
||||
const char* sql = "SELECT id, title, content, solfeggio_freq_hz, category, is_active, "
|
||||
"selected, category_id, solfeggio_frequency_id, required_level_id FROM proclamations;";
|
||||
"selected, category_id, solfeggio_frequency_id FROM proclamations;";
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
return result;
|
||||
|
||||
@@ -17,7 +17,6 @@ struct ProclamationRecord {
|
||||
bool selected = false;
|
||||
int category_id = 0; // maps to ProclamationCategoryRecord::id
|
||||
int solfeggio_frequency_id = 0; // maps to the solfeggio frequency preset
|
||||
int required_level_id = 1; // minimum SubscriptionLevel id to unlock it
|
||||
};
|
||||
|
||||
struct ProclamationCategoryRecord {
|
||||
|
||||
@@ -6,9 +6,6 @@ namespace radiantwave::db {
|
||||
// device configuration lives in the local database.
|
||||
constexpr const char* KV_EMAIL_ADDRESS = "email_address";
|
||||
constexpr const char* KV_LICENSE_KEY = "license_key";
|
||||
constexpr const char* KV_SUBSCRIPTION_LEVEL_ID = "subscription_level_id";
|
||||
constexpr const char* KV_SUBSCRIPTION_LEVEL_NAME = "subscription_level_name";
|
||||
constexpr const char* KV_SUBSCRIPTION_LEVEL_DESCRIPTION = "subscription_level_description";
|
||||
|
||||
// Unix epoch seconds of the last successful license validation against the
|
||||
// portal. Used to grant a 7-day offline grace window so transient network
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "date_format.h"
|
||||
#include "audio/audio_downloader.h"
|
||||
#include "db/db_keys.h"
|
||||
#include "network/license_service.h"
|
||||
#include "network/proclamation_sync.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
@@ -218,20 +217,12 @@ void Home::render(SDL_Renderer *renderer)
|
||||
break;
|
||||
}
|
||||
|
||||
int subscriptionLevelId = state_
|
||||
? radiantwave::license::getStoredSubscriptionLevelId(state_->db)
|
||||
: 0;
|
||||
std::string subscriptionValue = subscriptionLevelId > 0
|
||||
? radiantwave::license::subscriptionLevelName(subscriptionLevelId)
|
||||
: std::string("Not set");
|
||||
|
||||
time_t now = std::time(nullptr);
|
||||
std::vector<std::string> names = {
|
||||
"Version:",
|
||||
"License:",
|
||||
"Key:",
|
||||
"Email:",
|
||||
"Subscription:",
|
||||
"WiFi:",
|
||||
"Internet:",
|
||||
"Audio:",
|
||||
@@ -243,7 +234,6 @@ void Home::render(SDL_Renderer *renderer)
|
||||
licenseValue,
|
||||
license_.empty() ? std::string("Not set") : license_,
|
||||
email_.empty() ? std::string("Not set") : email_,
|
||||
subscriptionValue,
|
||||
ssid_.empty() ? std::string("Not connected") : ssid_,
|
||||
internet_ ? std::string("Connected") : std::string("Offline"),
|
||||
audioStatus(),
|
||||
@@ -377,7 +367,6 @@ void Home::createTextures(AppState &state)
|
||||
"License:",
|
||||
"Key:",
|
||||
"Email:",
|
||||
"Subscription:",
|
||||
"WiFi:",
|
||||
"Internet:",
|
||||
"Audio:",
|
||||
|
||||
@@ -95,14 +95,13 @@ void LicenseKeyPage::submit()
|
||||
|
||||
// Both the email address and the license key are required to validate.
|
||||
// When the email is missing the key is saved but the check is skipped.
|
||||
std::string levelName;
|
||||
std::string errorMessage;
|
||||
const radiantwave::license::Result result = radiantwave::license::validateStoredLicense(
|
||||
state_->db, "", &levelName, &errorMessage);
|
||||
state_->db, "", &errorMessage);
|
||||
|
||||
switch (result) {
|
||||
case radiantwave::license::Result::Ok:
|
||||
statusMessage_ = "License validated (" + levelName + ")";
|
||||
statusMessage_ = "License validated";
|
||||
break;
|
||||
case radiantwave::license::Result::NotConfigured:
|
||||
statusMessage_ = "Saved - add your email address to validate the license";
|
||||
|
||||
@@ -189,27 +189,9 @@ bool parseLicenseJson(const std::string& body, LicenseInfo& out) {
|
||||
out.key = root.stringField("key");
|
||||
out.email_address = root.stringField("email_address");
|
||||
|
||||
// subscription_level is an object {id,name,description} in the updated API
|
||||
// and a bare level-name string in the current one. Tolerate both, plus a
|
||||
// flat {subscription_level, subscription_level_id, subscription_level_description}.
|
||||
const radiantwave::json::JsonValue* level = root.find("subscription_level");
|
||||
if (level && level->type == radiantwave::json::JsonValue::Type::Object) {
|
||||
out.subscription_level.id = static_cast<int>(level->numberField("id"));
|
||||
out.subscription_level.name = level->stringField("name");
|
||||
out.subscription_level.description = level->stringField("description");
|
||||
} else if (level && level->type == radiantwave::json::JsonValue::Type::String) {
|
||||
out.subscription_level.name = level->str;
|
||||
}
|
||||
|
||||
if (out.subscription_level.name.empty()) {
|
||||
out.subscription_level.name = root.stringField("subscription_level");
|
||||
}
|
||||
if (out.subscription_level.id == 0) {
|
||||
out.subscription_level.id = static_cast<int>(root.numberField("subscription_level_id"));
|
||||
}
|
||||
if (out.subscription_level.description.empty()) {
|
||||
out.subscription_level.description = root.stringField("subscription_level_description");
|
||||
}
|
||||
// Any other fields (e.g. the legacy `subscription_level`) are ignored. A
|
||||
// successful validation itself is the "subscribed" flag; the kiosk treats
|
||||
// a 2xx response as an active license.
|
||||
|
||||
return !out.key.empty();
|
||||
}
|
||||
@@ -251,12 +233,12 @@ bool parseCreateAccountJson(const std::string& body, AccountBody& out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
AccountResult createAccount(const std::string& email, const std::string& subscription_level,
|
||||
const std::string& name, const std::string& apiKey) {
|
||||
AccountResult createAccount(const std::string& email, const std::string& name,
|
||||
const std::string& apiKey) {
|
||||
AccountResult result;
|
||||
|
||||
std::string jsonBody =
|
||||
"{\"email\":\"" + email + "\",\"subscription_level\":\"" + subscription_level + "\"";
|
||||
"{\"email\":\"" + email + "\"";
|
||||
if (!name.empty()) {
|
||||
jsonBody += ",\"name\":\"" + name + "\"";
|
||||
}
|
||||
|
||||
@@ -79,19 +79,9 @@ bool fetchColorPalettes(std::vector<ColorPalette>& out, const std::string& apiKe
|
||||
|
||||
// ─── POST /license ─────────────────────────────────────────────────────
|
||||
|
||||
// A subscription level as described by the portal's `SubscriptionLevel`
|
||||
// table. `name` is always populated; `id`/`description` may be 0/empty when
|
||||
// the API only reported the level name.
|
||||
struct SubscriptionLevel {
|
||||
int id = 0;
|
||||
std::string name;
|
||||
std::string description;
|
||||
};
|
||||
|
||||
struct LicenseInfo {
|
||||
std::string key;
|
||||
std::string email_address;
|
||||
SubscriptionLevel subscription_level;
|
||||
};
|
||||
|
||||
// Parse the 200 JSON payload returned by the license endpoint.
|
||||
@@ -107,6 +97,7 @@ struct LicenseResult {
|
||||
|
||||
// Validate a license key / email pair against the portal. Never throws; failures
|
||||
// (network, HTTP error status) are reported through the returned LicenseResult.
|
||||
// A 2xx response means the license is valid and the subscription is active.
|
||||
LicenseResult validateLicense(const std::string& key, const std::string& email,
|
||||
const std::string& apiKey = "");
|
||||
|
||||
@@ -131,7 +122,7 @@ struct AccountResult {
|
||||
|
||||
// Creates (or reuses) a user account and issues a license key. The key is emailed
|
||||
// to the user and is NOT part of the response. `name` may be empty.
|
||||
AccountResult createAccount(const std::string& email, const std::string& subscription_level,
|
||||
const std::string& name = "", const std::string& apiKey = "");
|
||||
AccountResult createAccount(const std::string& email, const std::string& name = "",
|
||||
const std::string& apiKey = "");
|
||||
|
||||
} // namespace radiantwave::api
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
#include "db/db_keys.h"
|
||||
#include "network/api_client.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace radiantwave::license {
|
||||
|
||||
@@ -19,9 +18,7 @@ long long nowEpochSeconds() {
|
||||
|
||||
Result validateStoredLicense(radiantwave::db::DatabaseManager& db,
|
||||
const std::string& apiKey,
|
||||
std::string* outLevelName,
|
||||
std::string* outErrorMessage) {
|
||||
if (outLevelName) outLevelName->clear();
|
||||
if (outErrorMessage) outErrorMessage->clear();
|
||||
|
||||
const std::string email = db.getKV(radiantwave::db::KV_EMAIL_ADDRESS);
|
||||
@@ -42,21 +39,10 @@ Result validateStoredLicense(radiantwave::db::DatabaseManager& db,
|
||||
return Result::Invalid;
|
||||
}
|
||||
|
||||
const auto& level = result.info.subscription_level;
|
||||
int levelId = level.id;
|
||||
if (levelId <= 0) {
|
||||
// The portal may only report the level name (e.g. "Elite"). Derive the
|
||||
// numeric id from the name so the Home footer and proclamation gating
|
||||
// can rank the level even when the response omits the id.
|
||||
levelId = subscriptionLevelIdForName(level.name);
|
||||
}
|
||||
db.setKV(radiantwave::db::KV_SUBSCRIPTION_LEVEL_ID,
|
||||
levelId > 0 ? std::to_string(levelId) : std::string());
|
||||
db.setKV(radiantwave::db::KV_SUBSCRIPTION_LEVEL_NAME, level.name);
|
||||
db.setKV(radiantwave::db::KV_SUBSCRIPTION_LEVEL_DESCRIPTION, level.description);
|
||||
// A successful validation is the "subscribed" flag: the license is valid
|
||||
// and the subscription is active. Only the "last verified" stamp is stored
|
||||
// (it drives the offline grace window).
|
||||
markLicenseVerified(db);
|
||||
|
||||
if (outLevelName) *outLevelName = level.name;
|
||||
return Result::Ok;
|
||||
}
|
||||
|
||||
@@ -76,33 +62,4 @@ bool isLicenseWithinGracePeriod(const radiantwave::db::DatabaseManager& db) {
|
||||
return nowEpochSeconds() - stamp < GRACE_PERIOD_SECONDS;
|
||||
}
|
||||
|
||||
int subscriptionLevelIdForName(const std::string& name) {
|
||||
std::string lower;
|
||||
lower.reserve(name.size());
|
||||
for (char c : name) {
|
||||
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(c))));
|
||||
}
|
||||
if (lower == "basic") return 1;
|
||||
if (lower == "advanced") return 2;
|
||||
if (lower == "elite") return 3;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int getStoredSubscriptionLevelId(const radiantwave::db::DatabaseManager& db) {
|
||||
const std::string raw = db.getKV(radiantwave::db::KV_SUBSCRIPTION_LEVEL_ID);
|
||||
if (raw.empty()) return 0;
|
||||
char* end = nullptr;
|
||||
const long id = std::strtol(raw.c_str(), &end, 10);
|
||||
return (end && *end == '\0' && id > 0) ? static_cast<int>(id) : 0;
|
||||
}
|
||||
|
||||
std::string subscriptionLevelName(int levelId) {
|
||||
switch (levelId) {
|
||||
case 1: return "Basic";
|
||||
case 2: return "Advanced";
|
||||
case 3: return "Elite";
|
||||
default: return "Subscription Level " + std::to_string(levelId);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace radiantwave::license
|
||||
|
||||
@@ -5,11 +5,8 @@
|
||||
|
||||
namespace radiantwave::license {
|
||||
|
||||
// NOTE: the system_kv keys where the validated subscription level is persisted
|
||||
// live in db/db_keys.h (KV_SUBSCRIPTION_LEVEL_*).
|
||||
|
||||
enum class Result {
|
||||
Ok, // license valid; subscription level persisted to the DB
|
||||
Ok, // license valid; subscription active
|
||||
NotConfigured, // email and/or license key missing from the DB
|
||||
Invalid, // API rejected the pair (bad key, wrong user, inactive, ...)
|
||||
NetworkError, // the request never completed
|
||||
@@ -22,29 +19,16 @@ enum class Result {
|
||||
constexpr long long GRACE_PERIOD_SECONDS = 7LL * 24 * 60 * 60;
|
||||
|
||||
// Validates the stored license key against the stored email address via the
|
||||
// portal API. On success the subscription level (id/name/description) is
|
||||
// written to the DB; on failure the stored level is left untouched so a
|
||||
// portal API. A successful validation is the device's "subscribed" flag — no
|
||||
// separate subscription state is tracked. On failure nothing is written, so a
|
||||
// previous good value survives a transient outage. Returns NotConfigured
|
||||
// without touching the network when either value is missing. Never throws.
|
||||
//
|
||||
// `outLevelName` / `outErrorMessage` are optional out-params.
|
||||
// `outErrorMessage` is an optional out-param.
|
||||
Result validateStoredLicense(radiantwave::db::DatabaseManager& db,
|
||||
const std::string& apiKey = "",
|
||||
std::string* outLevelName = nullptr,
|
||||
std::string* outErrorMessage = nullptr);
|
||||
|
||||
// Returns the subscription level id currently stored after a successful
|
||||
// license validation. Returns 0 when no validated license is on record.
|
||||
int getStoredSubscriptionLevelId(const radiantwave::db::DatabaseManager& db);
|
||||
|
||||
// Human-readable name for a subscription level id (Basic / Advanced / Elite).
|
||||
// Falls back to a generic label for unknown ids.
|
||||
std::string subscriptionLevelName(int levelId);
|
||||
|
||||
// Numeric id for a subscription level name (case-insensitive match against
|
||||
// Basic / Advanced / Elite). Returns 0 for an unknown or empty name.
|
||||
int subscriptionLevelIdForName(const std::string& name);
|
||||
|
||||
// Records the current time as the "last verified" stamp of the license. Called
|
||||
// after every successful server validation so the offline grace window starts
|
||||
// (or is extended) from the most recent confirmation.
|
||||
|
||||
@@ -67,8 +67,6 @@ bool parseProclamationsJson(const std::string& body,
|
||||
rec.content = stripHtml(entry.stringField("content"));
|
||||
rec.category_id = static_cast<int>(entry.numberField("categoryId"));
|
||||
rec.solfeggio_frequency_id = static_cast<int>(entry.numberField("solfeggioFrequencyId"));
|
||||
rec.required_level_id = static_cast<int>(entry.numberField("subscriptionLevelId"));
|
||||
if (rec.required_level_id <= 0) rec.required_level_id = 1;
|
||||
out.push_back(std::move(rec));
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "proclamation_options_page.h"
|
||||
#include "app_state.h"
|
||||
#include "network/license_service.h"
|
||||
#include "network/proclamation_sync.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <algorithm>
|
||||
@@ -9,8 +8,6 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint64_t kFlashDurationMs = 700;
|
||||
|
||||
// Case-insensitive word-prefix match: true when any word in `text` (split on
|
||||
// non-alphanumeric boundaries) starts with `query`. Both must already be
|
||||
// lowercase. Mirrors the old Go browser's matching behavior.
|
||||
@@ -45,9 +42,6 @@ void ProclamationOptionsPage::onPush(AppState& state)
|
||||
createFonts();
|
||||
SDL_StartTextInput(state.window);
|
||||
|
||||
int level = radiantwave::license::getStoredSubscriptionLevelId(state.db);
|
||||
subscriptionLevelId_ = level > 0 ? level : 1;
|
||||
|
||||
// TODO(portal #4/#5): once the portal exposes color palettes, sync them
|
||||
// alongside proclamations here (sync::syncColorPalettesFromApi after this
|
||||
// succeeds) so palette updates reach the scroller on every manual refresh.
|
||||
@@ -83,9 +77,6 @@ void ProclamationOptionsPage::onResume(AppState& state)
|
||||
createFonts();
|
||||
SDL_StartTextInput(state.window);
|
||||
|
||||
int level = radiantwave::license::getStoredSubscriptionLevelId(state.db);
|
||||
subscriptionLevelId_ = level > 0 ? level : 1;
|
||||
|
||||
loadFromDb();
|
||||
rebuildItems();
|
||||
rebuildVisible();
|
||||
@@ -302,27 +293,6 @@ void ProclamationOptionsPage::toggleItem()
|
||||
return;
|
||||
}
|
||||
|
||||
// Selecting a proclamation whose required subscription level is above the
|
||||
// one attached to the stored license key is rejected: the name flashes red
|
||||
// and a hint is shown up top.
|
||||
if (!item.selected) {
|
||||
int requiredLevel = 1;
|
||||
for (const auto& p : proclamations_) {
|
||||
if (p.id == item.id) {
|
||||
requiredLevel = p.required_level_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (requiredLevel > subscriptionLevelId_) {
|
||||
flashItemIndex_ = static_cast<int>(visible_[highlightIndex_]);
|
||||
flashUntilMs_ = SDL_GetTicks() + kFlashDurationMs;
|
||||
statusMessage_ = "This proclamation is available on " +
|
||||
radiantwave::license::subscriptionLevelName(requiredLevel) +
|
||||
" systems";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
item.selected = !item.selected;
|
||||
for (auto& p : proclamations_) {
|
||||
if (p.id == item.id) {
|
||||
@@ -390,9 +360,9 @@ int ProclamationOptionsPage::activeListSize() const
|
||||
}
|
||||
|
||||
// Toggles the highlighted search result. Selection rules mirror toggleItem():
|
||||
// the always-on top-level proclamation is not togglable, and locked
|
||||
// proclamations flash red. Selection state is synced back into items_ so the
|
||||
// browse tree stays consistent when search mode ends.
|
||||
// the always-on top-level proclamation is not togglable. Selection state is
|
||||
// synced back into items_ so the browse tree stays consistent when search mode
|
||||
// ends.
|
||||
void ProclamationOptionsPage::toggleSearchItem()
|
||||
{
|
||||
if (searchResults_.empty()) return;
|
||||
@@ -403,17 +373,6 @@ void ProclamationOptionsPage::toggleSearchItem()
|
||||
return;
|
||||
}
|
||||
|
||||
if (!p.selected) {
|
||||
if (p.required_level_id > subscriptionLevelId_) {
|
||||
flashItemIndex_ = static_cast<int>(searchResults_[highlightIndex_]);
|
||||
flashUntilMs_ = SDL_GetTicks() + kFlashDurationMs;
|
||||
statusMessage_ = "This proclamation is available on " +
|
||||
radiantwave::license::subscriptionLevelName(p.required_level_id) +
|
||||
" systems";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
p.selected = !p.selected;
|
||||
for (auto& item : items_) {
|
||||
if (item.type == FlatItem::Type::Proclamation && item.id == p.id) {
|
||||
@@ -609,9 +568,6 @@ void ProclamationOptionsPage::render(SDL_Renderer* renderer)
|
||||
SDL_Color gray = {170, 170, 170, 255};
|
||||
SDL_Color categoryBlue = {100, 180, 255, 255};
|
||||
SDL_Color selectedOrange = {255, 170, 60, 255};
|
||||
SDL_Color lockedRed = {235, 70, 70, 255};
|
||||
|
||||
bool flashing = SDL_GetTicks() < flashUntilMs_;
|
||||
|
||||
float titleY = 50.0f;
|
||||
float titleH = 0.0f;
|
||||
@@ -718,9 +674,7 @@ void ProclamationOptionsPage::render(SDL_Renderer* renderer)
|
||||
float y = listTop + rowIndex * rowPitch;
|
||||
|
||||
SDL_Color color = white;
|
||||
if (flashing && searchResults_[i] == static_cast<size_t>(flashItemIndex_)) {
|
||||
color = lockedRed;
|
||||
} else if (p.selected) {
|
||||
if (p.selected) {
|
||||
color = selectedOrange;
|
||||
}
|
||||
|
||||
@@ -771,9 +725,7 @@ void ProclamationOptionsPage::render(SDL_Renderer* renderer)
|
||||
}
|
||||
|
||||
SDL_Color color = white;
|
||||
if (flashing && static_cast<int>(visible_[i]) == flashItemIndex_) {
|
||||
color = lockedRed;
|
||||
} else if (item.type == FlatItem::Type::RootCategory || item.type == FlatItem::Type::SubCategory) {
|
||||
if (item.type == FlatItem::Type::RootCategory || item.type == FlatItem::Type::SubCategory) {
|
||||
color = categoryBlue;
|
||||
} else if (item.selected || item.isDefault) {
|
||||
color = selectedOrange;
|
||||
|
||||
@@ -79,10 +79,6 @@ private:
|
||||
bool searchActive_ = false;
|
||||
std::string searchQuery_;
|
||||
|
||||
int subscriptionLevelId_ = 1; // stored license level; 1 (Basic) without one
|
||||
int flashItemIndex_ = -1; // items_ index flashing red (locked attempt)
|
||||
uint64_t flashUntilMs_ = 0; // SDL_GetTicks() deadline for the flash
|
||||
|
||||
TTF_Font* fontTitle_ = nullptr;
|
||||
TTF_Font* fontItem_ = nullptr;
|
||||
TTF_Font* fontSmall_ = nullptr;
|
||||
|
||||
Reference in New Issue
Block a user