747 lines
26 KiB
C++
747 lines
26 KiB
C++
#include "FontDownloadActivity.h"
|
|
|
|
#include <ArduinoJson.h>
|
|
#include <GfxRenderer.h>
|
|
#include <HalStorage.h>
|
|
#include <I18n.h>
|
|
#include <Logging.h>
|
|
#include <WiFi.h>
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
|
|
#include "MappedInputManager.h"
|
|
#include "SdCardFontGlobals.h"
|
|
#include "SilentRestart.h"
|
|
#include "activities/network/WifiSelectionActivity.h"
|
|
#include "activities/util/ConfirmationActivity.h"
|
|
#include "components/UITheme.h"
|
|
#include "fontIds.h"
|
|
#include "network/HttpDownloader.h"
|
|
|
|
FontDownloadActivity::FontDownloadActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
|
: Activity("FontDownload", renderer, mappedInput), fontInstaller_(sdFontSystem.registry()) {}
|
|
|
|
// --- Lifecycle ---
|
|
|
|
void FontDownloadActivity::onEnter() {
|
|
Activity::onEnter();
|
|
WiFi.mode(WIFI_STA);
|
|
startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
|
|
[this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); });
|
|
}
|
|
|
|
void FontDownloadActivity::onExit() {
|
|
Activity::onExit();
|
|
|
|
if (WiFi.getMode() != WIFI_MODE_NULL) {
|
|
WiFi.disconnect(false);
|
|
delay(30);
|
|
silentRestart();
|
|
}
|
|
}
|
|
|
|
void FontDownloadActivity::onWifiSelectionComplete(const bool success) {
|
|
if (!success) {
|
|
finish();
|
|
return;
|
|
}
|
|
|
|
{
|
|
RenderLock lock(*this);
|
|
state_ = LOADING_MANIFEST;
|
|
}
|
|
requestUpdateAndWait();
|
|
|
|
if (!fetchAndParseManifest()) {
|
|
RenderLock lock(*this);
|
|
state_ = ERROR;
|
|
return;
|
|
}
|
|
|
|
{
|
|
RenderLock lock(*this);
|
|
state_ = FAMILY_LIST;
|
|
selectedIndex_ = 0;
|
|
previousActionCount_ = actionCount();
|
|
}
|
|
}
|
|
|
|
// --- Manifest fetching ---
|
|
|
|
bool FontDownloadActivity::fetchAndParseManifest() {
|
|
static constexpr const char* MANIFEST_TMP = "/fonts_manifest.tmp";
|
|
|
|
auto result = HttpDownloader::downloadToFile(FONT_MANIFEST_URL, MANIFEST_TMP, nullptr);
|
|
if (result != HttpDownloader::OK) {
|
|
LOG_ERR("FONT", "Failed to fetch manifest from %s", FONT_MANIFEST_URL);
|
|
errorMessage_ = "Failed to fetch font list";
|
|
Storage.remove(MANIFEST_TMP);
|
|
return false;
|
|
}
|
|
|
|
FsFile manifestFile;
|
|
if (!Storage.openFileForRead("FONT", MANIFEST_TMP, manifestFile)) {
|
|
LOG_ERR("FONT", "Failed to open temp manifest");
|
|
Storage.remove(MANIFEST_TMP);
|
|
errorMessage_ = "Failed to read font list";
|
|
return false;
|
|
}
|
|
|
|
JsonDocument doc;
|
|
DeserializationError err = deserializeJson(doc, manifestFile);
|
|
manifestFile.close();
|
|
Storage.remove(MANIFEST_TMP);
|
|
|
|
if (err) {
|
|
LOG_ERR("FONT", "Manifest parse error: %s", err.c_str());
|
|
errorMessage_ = "Invalid font manifest";
|
|
return false;
|
|
}
|
|
|
|
int version = doc["version"] | 0;
|
|
// v1 (legacy, no crc32) and v2 (with crc32) are both accepted; crc check is
|
|
// skipped per-file when the field is absent. See upstream PR #1904 for the
|
|
// CRC32 design we mirror.
|
|
if (version != 1 && version != 2) {
|
|
LOG_ERR("FONT", "Unsupported manifest version: %d", version);
|
|
errorMessage_ = "Unsupported manifest version";
|
|
return false;
|
|
}
|
|
|
|
baseUrl_ = doc["baseUrl"] | "";
|
|
families_.clear();
|
|
|
|
JsonArray familiesArr = doc["families"].as<JsonArray>();
|
|
families_.reserve(familiesArr.size());
|
|
|
|
for (JsonObject fObj : familiesArr) {
|
|
ManifestFamily family;
|
|
family.name = fObj["name"] | "";
|
|
family.description = fObj["description"] | "";
|
|
|
|
for (JsonVariant s : fObj["styles"].as<JsonArray>()) {
|
|
family.styles.push_back(s.as<std::string>());
|
|
}
|
|
|
|
family.totalSize = 0;
|
|
for (JsonObject fileObj : fObj["files"].as<JsonArray>()) {
|
|
ManifestFile file;
|
|
file.name = fileObj["name"] | "";
|
|
file.size = fileObj["size"] | 0;
|
|
if (fileObj["crc32"].is<uint32_t>()) {
|
|
file.crc32 = fileObj["crc32"].as<uint32_t>();
|
|
file.hasCrc32 = true;
|
|
}
|
|
family.totalSize += file.size;
|
|
family.files.push_back(std::move(file));
|
|
}
|
|
|
|
family.installed = fontInstaller_.isFamilyInstalled(family.name.c_str());
|
|
|
|
if (family.installed) {
|
|
for (const auto& file : family.files) {
|
|
std::string localFilename = file.name;
|
|
std::string familyPrefix = family.name + "/";
|
|
if (localFilename.find(familyPrefix) == 0) {
|
|
localFilename = localFilename.substr(familyPrefix.length());
|
|
}
|
|
|
|
char path[128];
|
|
FontInstaller::buildFontPath(family.name.c_str(), localFilename.c_str(), path, sizeof(path));
|
|
FsFile f;
|
|
if (Storage.openFileForRead("FONT", path, f)) {
|
|
size_t actual = f.fileSize();
|
|
f.close();
|
|
if (actual != file.size) {
|
|
family.hasUpdate = true;
|
|
break;
|
|
}
|
|
} else {
|
|
family.hasUpdate = true;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
// Surface leftover staging from a previously interrupted download so the
|
|
// UI can offer "Resume" instead of restarting from scratch.
|
|
char stagingDir[128];
|
|
FontInstaller::buildStagingDirPath(family.name.c_str(), stagingDir, sizeof(stagingDir));
|
|
family.hasResumableDownload = Storage.exists(stagingDir);
|
|
}
|
|
|
|
families_.push_back(std::move(family));
|
|
}
|
|
|
|
LOG_DBG("FONT", "Manifest loaded: %zu families", families_.size());
|
|
return true;
|
|
}
|
|
|
|
// --- Download ---
|
|
|
|
void FontDownloadActivity::downloadAll() {
|
|
cancelRequested_ = false;
|
|
for (size_t i = 0; i < families_.size(); i++) {
|
|
if (families_[i].installed) continue;
|
|
downloadFamily(families_[i]);
|
|
if (state_ == ERROR || cancelRequested_) return;
|
|
}
|
|
|
|
RenderLock lock(*this);
|
|
state_ = COMPLETE;
|
|
}
|
|
|
|
void FontDownloadActivity::updateAll() {
|
|
cancelRequested_ = false;
|
|
for (size_t i = 0; i < families_.size(); i++) {
|
|
if (!families_[i].installed || !families_[i].hasUpdate) continue;
|
|
downloadFamily(families_[i]);
|
|
if (state_ == ERROR || cancelRequested_) return;
|
|
}
|
|
|
|
RenderLock lock(*this);
|
|
state_ = COMPLETE;
|
|
}
|
|
|
|
size_t FontDownloadActivity::totalUninstalledSize() const {
|
|
size_t total = 0;
|
|
for (const auto& f : families_) {
|
|
if (!f.installed) total += f.totalSize;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
size_t FontDownloadActivity::totalUpdateSize() const {
|
|
size_t total = 0;
|
|
for (const auto& f : families_) {
|
|
if (f.installed && f.hasUpdate) total += f.totalSize;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
void FontDownloadActivity::syncSelectedIndexForNewActionCount() {
|
|
const int currentActionCount = actionCount();
|
|
if (currentActionCount == previousActionCount_) {
|
|
return;
|
|
}
|
|
|
|
int newIndex = selectedIndex_;
|
|
if (selectedIndex_ >= previousActionCount_) {
|
|
const int familyIndex = selectedIndex_ - previousActionCount_;
|
|
newIndex = familyIndex + currentActionCount;
|
|
} else if (selectedIndex_ >= currentActionCount) {
|
|
newIndex = currentActionCount;
|
|
}
|
|
|
|
if (newIndex >= listItemCount()) {
|
|
newIndex = std::max(0, listItemCount() - 1);
|
|
}
|
|
|
|
selectedIndex_ = newIndex;
|
|
previousActionCount_ = currentActionCount;
|
|
}
|
|
|
|
bool FontDownloadActivity::hasDownloadCandidates() const {
|
|
for (const auto& f : families_) {
|
|
if (!f.installed) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FontDownloadActivity::hasUpdateCandidates() const {
|
|
for (const auto& f : families_) {
|
|
if (f.installed && f.hasUpdate) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
|
|
cancelRequested_ = false;
|
|
{
|
|
RenderLock lock(*this);
|
|
state_ = DOWNLOADING;
|
|
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
|
|
currentFileIndex_ = 0;
|
|
currentFileTotal_ = family.files.size();
|
|
fileProgress_ = 0;
|
|
fileTotal_ = 0;
|
|
}
|
|
requestUpdateAndWait();
|
|
|
|
char liveDir[128];
|
|
char stagingDir[128];
|
|
char backupDir[128];
|
|
snprintf(liveDir, sizeof(liveDir), "%s/%s", SdCardFontRegistry::FONTS_DIR, family.name.c_str());
|
|
FontInstaller::buildStagingDirPath(family.name.c_str(), stagingDir, sizeof(stagingDir));
|
|
FontInstaller::buildBackupDirPath(family.name.c_str(), backupDir, sizeof(backupDir));
|
|
|
|
// Resume-aware staging: if a __staging dir is left over from a previous
|
|
// interrupted download, keep it so files already on disk can be reused.
|
|
// Files are individually re-verified below (size + CRC) before being
|
|
// accepted, so half-written files are caught.
|
|
if (!Storage.exists(stagingDir) && !Storage.mkdir(stagingDir)) {
|
|
LOG_ERR("FONT", "Failed to create staging dir: %s", stagingDir);
|
|
RenderLock lock(*this);
|
|
state_ = ERROR;
|
|
pendingErrorAction_ = PendingFontAction::Download;
|
|
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
|
|
errorMessage_ = "Failed to create staging area";
|
|
return;
|
|
}
|
|
|
|
for (size_t i = 0; i < family.files.size(); i++) {
|
|
const auto& file = family.files[i];
|
|
|
|
{
|
|
RenderLock lock(*this);
|
|
currentFileIndex_ = i;
|
|
fileProgress_ = 0;
|
|
fileTotal_ = file.size;
|
|
lastProgressPercent_ = -1;
|
|
lastProgressUpdateMs_ = 0;
|
|
}
|
|
requestUpdateAndWait();
|
|
|
|
std::string localFilename = file.name;
|
|
std::string familyPrefix = family.name + "/";
|
|
if (localFilename.find(familyPrefix) == 0) {
|
|
localFilename = localFilename.substr(familyPrefix.length());
|
|
}
|
|
|
|
char stagedPath[128];
|
|
snprintf(stagedPath, sizeof(stagedPath), "%s/%s", stagingDir, localFilename.c_str());
|
|
|
|
// If this file is already present in staging from a previous run and
|
|
// matches the manifest, skip the download. CRC32 is checked when the
|
|
// manifest carries one (v2+); otherwise size + magic-byte check is the
|
|
// best we can do.
|
|
if (Storage.exists(stagedPath)) {
|
|
FsFile f;
|
|
bool sizeOk = false;
|
|
if (Storage.openFileForRead("FONT", stagedPath, f)) {
|
|
sizeOk = (f.fileSize() == file.size);
|
|
f.close();
|
|
}
|
|
bool crcOk = !file.hasCrc32;
|
|
if (sizeOk && file.hasCrc32) {
|
|
uint32_t actualCrc = 0;
|
|
if (FontInstaller::computeFileCrc32(stagedPath, actualCrc)) {
|
|
crcOk = (actualCrc == file.crc32);
|
|
}
|
|
}
|
|
if (sizeOk && crcOk && fontInstaller_.validateCpfontFile(stagedPath)) {
|
|
LOG_DBG("FONT", "Resuming: reusing %s", stagedPath);
|
|
fileProgress_ = file.size;
|
|
fileTotal_ = file.size;
|
|
continue;
|
|
}
|
|
LOG_DBG("FONT", "Resuming: re-downloading stale %s (sizeOk=%d crcOk=%d)", stagedPath, sizeOk, crcOk);
|
|
Storage.remove(stagedPath);
|
|
}
|
|
|
|
// Make sure parent directories exist for the file
|
|
std::string stagedPathStr(stagedPath);
|
|
size_t lastSlash = stagedPathStr.find_last_of('/');
|
|
if (lastSlash != std::string::npos) {
|
|
Storage.mkdir(stagedPathStr.substr(0, lastSlash).c_str());
|
|
}
|
|
|
|
std::string url = baseUrl_ + file.name;
|
|
|
|
// Give the previous TLS connection (manifest fetch on first iteration, or
|
|
// the previous file otherwise) time to fully release its socket and TLS
|
|
// buffers. Without this, http.GET() can fail immediately with -1
|
|
// (HTTPC_ERROR_CONNECTION_REFUSED) on the next request — same workaround
|
|
// the browser-driven install path uses in CrossPointWebServer.
|
|
delay(500);
|
|
|
|
auto result = HttpDownloader::downloadToFile(url, stagedPath, [this](unsigned int downloaded, unsigned int total) {
|
|
mappedInput.update();
|
|
fileProgress_ = downloaded;
|
|
fileTotal_ = total;
|
|
|
|
const unsigned long now = millis();
|
|
int percent = 0;
|
|
if (total > 0) {
|
|
percent = static_cast<int>((static_cast<unsigned long long>(downloaded) * 100ULL + total / 2) / total);
|
|
}
|
|
const bool percentChanged = percent != lastProgressPercent_;
|
|
const bool timeElapsed = lastProgressUpdateMs_ == 0 || now - lastProgressUpdateMs_ > 2000;
|
|
if ((percentChanged && timeElapsed) || downloaded == total) {
|
|
requestUpdate(true);
|
|
lastProgressPercent_ = percent;
|
|
lastProgressUpdateMs_ = now;
|
|
}
|
|
|
|
return !mappedInput.wasPressed(MappedInputManager::Button::Back);
|
|
});
|
|
|
|
if (result == HttpDownloader::ABORTED) {
|
|
LOG_INF("FONT", "Download cancelled: %s", file.name.c_str());
|
|
// Keep staging dir so the next launch can resume.
|
|
Storage.remove(stagedPath);
|
|
family.hasResumableDownload = !family.installed;
|
|
cancelRequested_ = true;
|
|
RenderLock lock(*this);
|
|
state_ = FAMILY_LIST;
|
|
return;
|
|
}
|
|
|
|
if (result != HttpDownloader::OK) {
|
|
LOG_ERR("FONT", "Download failed: %s (%d)", file.name.c_str(), result);
|
|
// Drop just the file that failed; keep already-downloaded siblings so
|
|
// the next retry resumes from here.
|
|
Storage.remove(stagedPath);
|
|
family.hasResumableDownload = !family.installed;
|
|
RenderLock lock(*this);
|
|
state_ = ERROR;
|
|
pendingErrorAction_ = PendingFontAction::Download;
|
|
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
|
|
errorMessage_ = "Download failed: " + file.name;
|
|
return;
|
|
}
|
|
|
|
// CRC32: matches upstream PR #1904 — catches truncated/torn writes.
|
|
if (file.hasCrc32) {
|
|
uint32_t actualCrc = 0;
|
|
if (!FontInstaller::computeFileCrc32(stagedPath, actualCrc)) {
|
|
LOG_ERR("FONT", "Failed to read for CRC: %s", stagedPath);
|
|
Storage.remove(stagedPath);
|
|
family.hasResumableDownload = !family.installed;
|
|
RenderLock lock(*this);
|
|
state_ = ERROR;
|
|
pendingErrorAction_ = PendingFontAction::Download;
|
|
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
|
|
errorMessage_ = "Failed to verify: " + file.name;
|
|
return;
|
|
}
|
|
if (actualCrc != file.crc32) {
|
|
LOG_ERR("FONT", "CRC32 mismatch for %s: got %08x expected %08x", file.name.c_str(), actualCrc, file.crc32);
|
|
Storage.remove(stagedPath);
|
|
family.hasResumableDownload = !family.installed;
|
|
RenderLock lock(*this);
|
|
state_ = ERROR;
|
|
pendingErrorAction_ = PendingFontAction::Download;
|
|
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
|
|
errorMessage_ = "Checksum mismatch: " + file.name;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!fontInstaller_.validateCpfontFile(stagedPath)) {
|
|
LOG_ERR("FONT", "Invalid .cpfont: %s", stagedPath);
|
|
Storage.remove(stagedPath);
|
|
family.hasResumableDownload = !family.installed;
|
|
RenderLock lock(*this);
|
|
state_ = ERROR;
|
|
pendingErrorAction_ = PendingFontAction::Download;
|
|
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
|
|
errorMessage_ = "Invalid font file: " + file.name;
|
|
return;
|
|
}
|
|
}
|
|
|
|
const bool hadLiveDir = Storage.exists(liveDir);
|
|
|
|
if (Storage.exists(backupDir) && !Storage.removeDir(backupDir)) {
|
|
LOG_ERR("FONT", "Failed to clean backup dir: %s", backupDir);
|
|
Storage.removeDir(stagingDir);
|
|
RenderLock lock(*this);
|
|
state_ = ERROR;
|
|
pendingErrorAction_ = PendingFontAction::Download;
|
|
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
|
|
errorMessage_ = "Failed to prepare backup area";
|
|
return;
|
|
}
|
|
|
|
if (hadLiveDir && !Storage.rename(liveDir, backupDir)) {
|
|
LOG_ERR("FONT", "Failed to move live family to backup: %s", liveDir);
|
|
Storage.removeDir(stagingDir);
|
|
RenderLock lock(*this);
|
|
state_ = ERROR;
|
|
pendingErrorAction_ = PendingFontAction::Download;
|
|
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
|
|
errorMessage_ = "Failed to replace installed font";
|
|
return;
|
|
}
|
|
|
|
if (!Storage.rename(stagingDir, liveDir)) {
|
|
LOG_ERR("FONT", "Failed to activate staged family: %s", stagingDir);
|
|
if (hadLiveDir && Storage.exists(backupDir)) {
|
|
Storage.rename(backupDir, liveDir);
|
|
}
|
|
Storage.removeDir(stagingDir);
|
|
RenderLock lock(*this);
|
|
state_ = ERROR;
|
|
pendingErrorAction_ = PendingFontAction::Download;
|
|
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
|
|
errorMessage_ = "Failed to finalize font install";
|
|
return;
|
|
}
|
|
|
|
if (Storage.exists(backupDir) && !Storage.removeDir(backupDir)) {
|
|
LOG_INF("FONT", "Failed to remove backup dir after successful install: %s", backupDir);
|
|
}
|
|
|
|
fontInstaller_.refreshRegistry();
|
|
family.installed = true;
|
|
family.hasUpdate = false;
|
|
family.hasResumableDownload = false;
|
|
syncSelectedIndexForNewActionCount();
|
|
|
|
RenderLock lock(*this);
|
|
state_ = COMPLETE;
|
|
}
|
|
|
|
void FontDownloadActivity::promptDeleteFamily(int familyIndex) {
|
|
if (familyIndex < 0 || familyIndex >= static_cast<int>(families_.size())) return;
|
|
const auto& family = families_[familyIndex];
|
|
const std::string heading = tr(STR_DELETE) + std::string("?");
|
|
const std::string body = family.name;
|
|
startActivityForResult(std::make_unique<ConfirmationActivity>(renderer, mappedInput, heading, body),
|
|
[this, familyIndex](const ActivityResult& result) {
|
|
if (result.isCancelled) return;
|
|
deleteFamilyAtIndex(familyIndex);
|
|
});
|
|
}
|
|
|
|
void FontDownloadActivity::deleteFamilyAtIndex(int familyIndex) {
|
|
if (familyIndex < 0 || familyIndex >= static_cast<int>(families_.size())) return;
|
|
|
|
auto& family = families_[familyIndex];
|
|
const auto result = fontInstaller_.deleteFamily(family.name.c_str());
|
|
if (result == FontInstaller::Error::OK) {
|
|
fontInstaller_.refreshRegistry();
|
|
family.installed = false;
|
|
family.hasUpdate = false;
|
|
syncSelectedIndexForNewActionCount();
|
|
pendingErrorAction_ = PendingFontAction::None;
|
|
errorMessage_.clear();
|
|
|
|
if (selectedIndex_ >= listItemCount()) {
|
|
selectedIndex_ = std::max(0, listItemCount() - 1);
|
|
}
|
|
|
|
RenderLock lock(*this);
|
|
state_ = FAMILY_LIST;
|
|
requestUpdate();
|
|
return;
|
|
}
|
|
|
|
std::string message = "Failed to delete font";
|
|
if (result == FontInstaller::Error::INVALID_FAMILY_NAME) {
|
|
message = "Invalid font family";
|
|
}
|
|
|
|
RenderLock lock(*this);
|
|
state_ = ERROR;
|
|
downloadingFamilyIndex_ = familyIndex;
|
|
pendingErrorAction_ = PendingFontAction::Delete;
|
|
errorMessage_ = message;
|
|
}
|
|
|
|
std::string FontDownloadActivity::confirmButtonLabel() const {
|
|
if (families_.empty()) return tr(STR_DOWNLOAD);
|
|
if (isDownloadAllSelected()) return tr(STR_DOWNLOAD);
|
|
if (isUpdateAllSelected()) return tr(STR_UPDATE);
|
|
const auto& family = families_[familyIndexFromList(selectedIndex_)];
|
|
if (family.installed && !family.hasUpdate) return tr(STR_DELETE);
|
|
if (family.hasUpdate) return tr(STR_UPDATE);
|
|
if (family.hasResumableDownload) return tr(STR_RESUME);
|
|
return tr(STR_DOWNLOAD);
|
|
}
|
|
|
|
// --- Input handling ---
|
|
|
|
void FontDownloadActivity::loop() {
|
|
if (state_ == FAMILY_LIST) {
|
|
syncSelectedIndexForNewActionCount();
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
|
finish();
|
|
return;
|
|
}
|
|
|
|
buttonNavigator_.onNextList(selectedIndex_, listItemCount(), [this] { requestUpdate(); });
|
|
buttonNavigator_.onPreviousList(selectedIndex_, listItemCount(), [this] { requestUpdate(); });
|
|
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
|
if (!families_.empty()) {
|
|
if (isDownloadAllSelected()) {
|
|
downloadAll();
|
|
requestUpdateAndWait();
|
|
} else if (isUpdateAllSelected()) {
|
|
updateAll();
|
|
requestUpdateAndWait();
|
|
} else {
|
|
const int familyIndex = familyIndexFromList(selectedIndex_);
|
|
const auto& family = families_[familyIndex];
|
|
if (family.installed && !family.hasUpdate) {
|
|
promptDeleteFamily(familyIndex);
|
|
} else {
|
|
downloadFamily(families_[familyIndex]);
|
|
requestUpdateAndWait();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (state_ == COMPLETE) {
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
|
|
mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
|
{
|
|
RenderLock lock(*this);
|
|
state_ = FAMILY_LIST;
|
|
}
|
|
requestUpdate();
|
|
}
|
|
} else if (state_ == ERROR) {
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
|
{
|
|
RenderLock lock(*this);
|
|
state_ = FAMILY_LIST;
|
|
}
|
|
requestUpdate();
|
|
} else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
|
if (downloadingFamilyIndex_ >= 0 && downloadingFamilyIndex_ < static_cast<int>(families_.size())) {
|
|
if (pendingErrorAction_ == PendingFontAction::Delete) {
|
|
deleteFamilyAtIndex(downloadingFamilyIndex_);
|
|
} else {
|
|
downloadFamily(families_[downloadingFamilyIndex_]);
|
|
}
|
|
requestUpdateAndWait();
|
|
} else {
|
|
{
|
|
RenderLock lock(*this);
|
|
state_ = FAMILY_LIST;
|
|
}
|
|
requestUpdate();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Rendering ---
|
|
|
|
std::string FontDownloadActivity::formatSize(size_t bytes) {
|
|
char buf[32];
|
|
if (bytes >= 1024 * 1024) {
|
|
snprintf(buf, sizeof(buf), "%.1f MB", static_cast<double>(bytes) / (1024.0 * 1024.0));
|
|
} else if (bytes >= 1024) {
|
|
snprintf(buf, sizeof(buf), "%.0f KB", static_cast<double>(bytes) / 1024.0);
|
|
} else {
|
|
snprintf(buf, sizeof(buf), "%zu B", bytes);
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
void FontDownloadActivity::render(RenderLock&&) {
|
|
const auto& metrics = UITheme::getInstance().getMetrics();
|
|
const auto pageWidth = renderer.getScreenWidth();
|
|
const auto pageHeight = renderer.getScreenHeight();
|
|
|
|
renderer.clearScreen();
|
|
|
|
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_FONT_MANAGER));
|
|
|
|
const auto lineHeight = renderer.getLineHeight(UI_10_FONT_ID);
|
|
const auto contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
|
|
const auto centerY = (pageHeight - lineHeight) / 2;
|
|
|
|
if (state_ == LOADING_MANIFEST) {
|
|
renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_LOADING_FONT_LIST));
|
|
} else if (state_ == FAMILY_LIST) {
|
|
syncSelectedIndexForNewActionCount();
|
|
if (families_.empty()) {
|
|
renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_NO_FONTS_AVAILABLE));
|
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
} else {
|
|
GUI.drawList(
|
|
renderer,
|
|
Rect{0, contentTop, pageWidth, pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing},
|
|
listItemCount(), selectedIndex_,
|
|
[this](int index) -> std::string {
|
|
if (hasDownloadCandidates()) {
|
|
if (index == 0) {
|
|
return std::string(tr(STR_DOWNLOAD_ALL)) + " (" + formatSize(totalUninstalledSize()) + ")";
|
|
}
|
|
if (hasUpdateCandidates() && index == 1) {
|
|
return std::string(tr(STR_UPDATE_ALL)) + " (" + formatSize(totalUpdateSize()) + ")";
|
|
}
|
|
} else if (hasUpdateCandidates() && index == 0) {
|
|
return std::string(tr(STR_UPDATE_ALL)) + " (" + formatSize(totalUpdateSize()) + ")";
|
|
}
|
|
return families_[familyIndexFromList(index)].name;
|
|
},
|
|
[this](int index) -> std::string {
|
|
if (hasDownloadCandidates()) {
|
|
if (index == 0) return "";
|
|
if (hasUpdateCandidates() && index == 1) return "";
|
|
} else if (hasUpdateCandidates() && index == 0) {
|
|
return "";
|
|
}
|
|
return families_[familyIndexFromList(index)].description;
|
|
},
|
|
nullptr,
|
|
[this](int index) -> std::string {
|
|
if (hasDownloadCandidates()) {
|
|
if (index == 0) return "";
|
|
if (hasUpdateCandidates() && index == 1) return "";
|
|
} else if (hasUpdateCandidates() && index == 0) {
|
|
return "";
|
|
}
|
|
const auto& f = families_[familyIndexFromList(index)];
|
|
if (f.hasUpdate) return tr(STR_UPDATE_AVAILABLE);
|
|
if (f.installed) return tr(STR_INSTALLED);
|
|
if (f.hasResumableDownload) return tr(STR_RESUME);
|
|
return "";
|
|
},
|
|
true);
|
|
|
|
const std::string confirmLabel = confirmButtonLabel();
|
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirmLabel.c_str(), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
}
|
|
} else if (state_ == DOWNLOADING) {
|
|
const auto& family = families_[downloadingFamilyIndex_];
|
|
|
|
std::string statusText = std::string(tr(STR_DOWNLOADING)) + " " + family.name + " (" +
|
|
std::to_string(currentFileIndex_ + 1) + "/" + std::to_string(currentFileTotal_) + ")";
|
|
renderer.drawCenteredText(UI_10_FONT_ID, centerY - lineHeight, statusText.c_str());
|
|
|
|
float progress = 0;
|
|
if (fileTotal_ > 0) {
|
|
progress = static_cast<float>(fileProgress_) / static_cast<float>(fileTotal_);
|
|
}
|
|
|
|
int barY = centerY + metrics.verticalSpacing;
|
|
GUI.drawProgressBar(
|
|
renderer,
|
|
Rect{metrics.contentSidePadding, barY, pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight},
|
|
static_cast<int>(progress * 100), 100);
|
|
|
|
int percentY = barY + metrics.progressBarHeight + metrics.verticalSpacing;
|
|
renderer.drawCenteredText(UI_10_FONT_ID, percentY,
|
|
(std::to_string(static_cast<int>(progress * 100)) + "%").c_str());
|
|
|
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
} else if (state_ == COMPLETE) {
|
|
renderer.drawCenteredText(UI_10_FONT_ID, centerY, tr(STR_FONT_INSTALLED), true, EpdFontFamily::BOLD);
|
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
} else if (state_ == ERROR) {
|
|
renderer.drawCenteredText(UI_10_FONT_ID, centerY - lineHeight, tr(STR_FONT_INSTALL_FAILED), true,
|
|
EpdFontFamily::BOLD);
|
|
if (!errorMessage_.empty()) {
|
|
renderer.drawCenteredText(UI_10_FONT_ID, centerY + metrics.verticalSpacing, errorMessage_.c_str());
|
|
}
|
|
const bool canResume = pendingErrorAction_ == PendingFontAction::Download && downloadingFamilyIndex_ >= 0 &&
|
|
downloadingFamilyIndex_ < static_cast<int>(families_.size()) &&
|
|
families_[downloadingFamilyIndex_].hasResumableDownload;
|
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), canResume ? tr(STR_RESUME) : tr(STR_RETRY), "", "");
|
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
}
|
|
|
|
renderer.displayBuffer();
|
|
}
|