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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace radiantwave::db {
|
||||
@@ -33,6 +34,29 @@ struct AudioManifestEntry {
|
||||
std::string md5;
|
||||
};
|
||||
|
||||
// Availability of one manifest entry, as published by the worker.
|
||||
// `available` means the file is present on disk (and therefore playable),
|
||||
// regardless of whether its checksum still needs refreshing.
|
||||
struct AudioFileStatus {
|
||||
std::string filename;
|
||||
std::string display_name;
|
||||
bool available = false;
|
||||
};
|
||||
|
||||
// A snapshot of the worker's most recent manifest reconciliation. Used by the
|
||||
// UI to warn the user when they start playback before every required audio
|
||||
// file has finished downloading.
|
||||
struct AudioDownloadSnapshot {
|
||||
// A manifest has been fetched at least once. Before this, the set of
|
||||
// expected files is unknown.
|
||||
bool manifestKnown = false;
|
||||
// Every manifest file is present on disk. False until the first manifest
|
||||
// is known.
|
||||
bool allAvailable = false;
|
||||
// One entry per manifest file, in manifest order.
|
||||
std::vector<AudioFileStatus> files;
|
||||
};
|
||||
|
||||
// Downloads audio assets from files.radiantwavetech.com in the background
|
||||
// and keeps the database's audio_files table in sync with what is actually
|
||||
// on disk.
|
||||
@@ -66,6 +90,9 @@ public:
|
||||
// Returns the current human-readable status for display (e.g. footer).
|
||||
std::string statusString() const;
|
||||
|
||||
// Returns the last published availability snapshot (thread-safe copy).
|
||||
AudioDownloadSnapshot snapshot() const;
|
||||
|
||||
static constexpr const char *AUDIO_BASE_URL =
|
||||
"https://files.radiantwavetech.com/audio/";
|
||||
static constexpr const char *MANIFEST_FILENAME = "checksums.md5";
|
||||
@@ -80,11 +107,18 @@ private:
|
||||
void setStatus(const std::string &msg);
|
||||
void wait(std::chrono::seconds seconds);
|
||||
|
||||
// Rebuilds `snapshot_` from the manifest and the set of filenames present
|
||||
// on disk. Called by the worker after its initial scan and after each
|
||||
// completed download so the UI sees progress.
|
||||
void publishSnapshot(const std::vector<AudioManifestEntry> &manifest,
|
||||
const std::unordered_set<std::string> &availableFilenames);
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
std::condition_variable cv_;
|
||||
std::thread worker_;
|
||||
std::atomic<bool> stop_{false};
|
||||
std::string status_;
|
||||
AudioDownloadSnapshot snapshot_;
|
||||
db::DatabaseManager *db_ = nullptr;
|
||||
};
|
||||
|
||||
@@ -109,4 +143,15 @@ int hzFromFilename(const std::string &filename);
|
||||
// Computes the lowercase MD5 hex of a file, or an empty string on failure.
|
||||
std::string computeFileMd5(const std::string &path);
|
||||
|
||||
// Fills `message` and returns true when starting playback should first show a
|
||||
// warning because required audio has not finished downloading:
|
||||
// * dynamic audio needs the whole manifest (the message names "ALL");
|
||||
// * a fixed track needs just its own file (the message names it).
|
||||
// Returns false when nothing required is missing, or when the selection is not
|
||||
// part of the manifest (so the downloader cannot confirm it either way).
|
||||
bool buildAudioDownloadWarning(const AudioDownloadSnapshot &snapshot,
|
||||
bool dynamicAudio,
|
||||
const std::string &selectedFilename,
|
||||
std::string &message);
|
||||
|
||||
} // namespace radiantwave::audio
|
||||
|
||||
Reference in New Issue
Block a user