Warning screen, smarter license re-validation, version reporting

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.
This commit is contained in:
NeilClack
2026-10-14 10:58:40 -05:00
parent 9b57e48969
commit c3b3d6774a
21 changed files with 999 additions and 52 deletions
+59
View File
@@ -261,6 +261,35 @@ std::string computeFileMd5(const std::string& path) {
return hex;
}
bool buildAudioDownloadWarning(const AudioDownloadSnapshot& snapshot,
bool dynamicAudio,
const std::string& selectedFilename,
std::string& message) {
if (dynamicAudio) {
if (snapshot.allAvailable) return false;
message =
"You currently have 'Dynamic Audio' selected, but some audio "
"files are still being downloaded. Some audio will not play until "
"these files have finished downloading.";
return true;
}
for (const auto& file : snapshot.files) {
if (file.filename != selectedFilename) continue;
if (file.available) return false;
const std::string name =
file.display_name.empty() ? file.filename : file.display_name;
message = "You currently have '" + name +
"' selected, but it is still being downloaded. No audio "
"will play until this file has finished downloading.";
return true;
}
// The selection is not part of the manifest (the manifest may not be known
// yet), so the downloader cannot confirm whether it is still downloading.
return false;
}
// ─── Lifecycle ──────────────────────────────────────────────────────────
AudioDownloader::~AudioDownloader() {
@@ -289,6 +318,30 @@ std::string AudioDownloader::statusString() const {
return status_;
}
AudioDownloadSnapshot AudioDownloader::snapshot() const {
std::lock_guard<std::mutex> lock(mutex_);
return snapshot_;
}
void AudioDownloader::publishSnapshot(
const std::vector<AudioManifestEntry>& manifest,
const std::unordered_set<std::string>& availableFilenames) {
AudioDownloadSnapshot snap;
snap.manifestKnown = true;
snap.allAvailable = !manifest.empty();
snap.files.reserve(manifest.size());
for (const auto& entry : manifest) {
const bool available = availableFilenames.count(entry.filename) != 0;
const std::string displayName =
entry.display_name.empty() ? humanizeFilename(entry.filename)
: entry.display_name;
snap.files.push_back({entry.filename, displayName, available});
if (!available) snap.allAvailable = false;
}
std::lock_guard<std::mutex> lock(mutex_);
snapshot_ = std::move(snap);
}
void AudioDownloader::setStatus(const std::string& msg) {
{
std::lock_guard<std::mutex> lock(mutex_);
@@ -359,6 +412,11 @@ void AudioDownloader::run() {
size_t toDownload = queue.size();
size_t downloaded = 0;
// Publish what is (and is not) on disk before downloads begin so the
// UI can warn immediately, then again as each file lands.
publishSnapshot(manifest, listed);
if (!queue.empty()) {
std::error_code ec;
std::filesystem::create_directories(dir, ec);
@@ -373,6 +431,7 @@ void AudioDownloader::run() {
// Publish immediately so a file that just finished downloading
// appears in the list without waiting for the rest.
syncNow();
publishSnapshot(manifest, listed);
}
}