## Summary * **What is the goal of this PR?** Separate the font manager's combined `Download / Update All` action into separate `Download All` and `Update All` rows * **What changes are included?** - Updated multiple i18n translation files to add `STR_UPDATE_ALL` and adjust `STR_DOWNLOAD_ALL` text. - Added separate handlers: `downloadAll()` for fonts that have been installed and `updateAll()` for fonts with updates. - `Download All` and `Update All` are only shown when applicable. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**PARTIALLY**_
590 lines
19 KiB
C++
590 lines
19 KiB
C++
#include "FontDownloadActivity.h"
|
|
|
|
#include <ArduinoJson.h>
|
|
#include <GfxRenderer.h>
|
|
#include <HalStorage.h>
|
|
#include <I18n.h>
|
|
#include <Logging.h>
|
|
#include <WiFi.h>
|
|
#include <esp_rom_crc.h>
|
|
|
|
#include "MappedInputManager.h"
|
|
#include "SdCardFontGlobals.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();
|
|
WiFi.disconnect(false);
|
|
delay(100);
|
|
WiFi.mode(WIFI_OFF);
|
|
delay(100);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// --- Manifest fetching ---
|
|
|
|
bool FontDownloadActivity::fetchAndParseManifest() {
|
|
// Download manifest to a temp file on SD card to avoid holding both
|
|
// TLS buffers and the full JSON string in RAM simultaneously.
|
|
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;
|
|
}
|
|
|
|
// HTTP client is now closed — TLS buffers freed. Parse JSON from file.
|
|
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;
|
|
if (version != FONTS_MANIFEST_VERSION) {
|
|
LOG_ERR("FONT", "Unsupported manifest version: %d", version);
|
|
errorMessage_ = "Unsupported manifest version";
|
|
return false;
|
|
}
|
|
|
|
baseUrl_ = doc["baseUrl"] | "";
|
|
families_.clear();
|
|
fontInstaller_.refreshRegistry();
|
|
|
|
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>()) {
|
|
LOG_ERR("FONT", "Malformed manifest file entry: missing or invalid crc32 for %s", file.name.c_str());
|
|
errorMessage_ = "Invalid font manifest";
|
|
return false;
|
|
}
|
|
file.crc32 = fileObj["crc32"].as<uint32_t>();
|
|
|
|
family.totalSize += file.size;
|
|
family.files.push_back(std::move(file));
|
|
}
|
|
|
|
family.installed = fontInstaller_.isFamilyInstalled(family.name.c_str());
|
|
|
|
// Detect updates by comparing manifest file sizes with files on disk.
|
|
// Not a checksum, but a size mismatch reliably indicates a rebuild in practice.
|
|
if (family.installed) {
|
|
for (const auto& file : family.files) {
|
|
char path[128];
|
|
FontInstaller::buildFontPath(family.name.c_str(), file.name.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 {
|
|
// File missing on disk but family dir exists — treat as update
|
|
family.hasUpdate = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
families_.push_back(std::move(family));
|
|
}
|
|
|
|
LOG_DBG("FONT", "Manifest loaded: %zu families", families_.size());
|
|
return true;
|
|
}
|
|
|
|
// --- Download ---
|
|
|
|
void FontDownloadActivity::downloadAll() {
|
|
for (size_t i = 0; i < families_.size(); i++) {
|
|
if (families_[i].installed) continue;
|
|
downloadFamily(families_[i]);
|
|
if (state_ == ERROR) return;
|
|
}
|
|
|
|
{
|
|
RenderLock lock(*this);
|
|
state_ = COMPLETE;
|
|
}
|
|
}
|
|
|
|
void FontDownloadActivity::updateAll() {
|
|
for (size_t i = 0; i < families_.size(); i++) {
|
|
if (!families_[i].hasUpdate) continue;
|
|
downloadFamily(families_[i]);
|
|
if (state_ == ERROR) return;
|
|
}
|
|
|
|
{
|
|
RenderLock lock(*this);
|
|
state_ = COMPLETE;
|
|
}
|
|
}
|
|
|
|
bool FontDownloadActivity::showDownloadAllRow() const {
|
|
for (const auto& f : families_) {
|
|
if (!f.installed) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FontDownloadActivity::showUpdateAllRow() const {
|
|
for (const auto& f : families_) {
|
|
if (f.hasUpdate) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
int FontDownloadActivity::specialRowCount() const {
|
|
return (showDownloadAllRow() ? 1 : 0) + (showUpdateAllRow() ? 1 : 0);
|
|
}
|
|
|
|
bool FontDownloadActivity::isDownloadAllRow(int index) const { return showDownloadAllRow() && index == 0; }
|
|
|
|
bool FontDownloadActivity::isUpdateAllRow(int index) const {
|
|
return showUpdateAllRow() && index == (showDownloadAllRow() ? 1 : 0);
|
|
}
|
|
|
|
int FontDownloadActivity::listItemCount() const {
|
|
return families_.empty() ? 0 : static_cast<int>(families_.size()) + specialRowCount();
|
|
}
|
|
|
|
size_t FontDownloadActivity::totalDownloadSize() 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.hasUpdate) total += f.totalSize;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
// Standard CRC32 matching zlib/Python zlib.crc32().
|
|
bool FontDownloadActivity::computeFileCrc32(const char* path, uint32_t& outCrc) {
|
|
FsFile f;
|
|
if (!Storage.openFileForRead("FONT", path, f)) {
|
|
return false;
|
|
}
|
|
constexpr size_t BUF_SIZE = 128;
|
|
uint8_t buf[BUF_SIZE];
|
|
uint32_t crc = 0;
|
|
while (f.available()) {
|
|
const int n = f.read(buf, BUF_SIZE);
|
|
if (n <= 0) break;
|
|
crc = esp_rom_crc32_le(crc, buf, static_cast<uint32_t>(n));
|
|
}
|
|
outCrc = crc;
|
|
return true;
|
|
}
|
|
|
|
void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
|
|
{
|
|
RenderLock lock(*this);
|
|
state_ = DOWNLOADING;
|
|
downloadingFamilyIndex_ = static_cast<int>(&family - families_.data());
|
|
currentFileIndex_ = 0;
|
|
currentFileTotal_ = family.files.size();
|
|
fileProgress_ = 0;
|
|
fileTotal_ = 0;
|
|
}
|
|
requestUpdateAndWait();
|
|
|
|
if (!fontInstaller_.ensureFamilyDir(family.name.c_str())) {
|
|
RenderLock lock(*this);
|
|
state_ = ERROR;
|
|
errorMessage_ = "Failed to create font directory";
|
|
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;
|
|
}
|
|
requestUpdateAndWait();
|
|
|
|
char destPath[128];
|
|
FontInstaller::buildFontPath(family.name.c_str(), file.name.c_str(), destPath, sizeof(destPath));
|
|
|
|
std::string url = baseUrl_ + file.name;
|
|
|
|
auto result = HttpDownloader::downloadToFile(url, destPath, [this](size_t downloaded, size_t total) {
|
|
fileProgress_ = downloaded;
|
|
fileTotal_ = total;
|
|
requestUpdate(true);
|
|
});
|
|
|
|
if (result != HttpDownloader::OK) {
|
|
LOG_ERR("FONT", "Download failed: %s (%d)", file.name.c_str(), result);
|
|
fontInstaller_.deleteFamily(family.name.c_str());
|
|
family.installed = false;
|
|
family.hasUpdate = false;
|
|
RenderLock lock(*this);
|
|
state_ = ERROR;
|
|
errorMessage_ = "Download failed: " + file.name;
|
|
return;
|
|
}
|
|
|
|
uint32_t actualCrc = 0;
|
|
if (!computeFileCrc32(destPath, actualCrc)) {
|
|
LOG_ERR("FONT", "Failed to open file for CRC check: %s", destPath);
|
|
fontInstaller_.deleteFamily(family.name.c_str());
|
|
family.installed = false;
|
|
family.hasUpdate = false;
|
|
RenderLock lock(*this);
|
|
state_ = ERROR;
|
|
errorMessage_ = "Failed to compute checksum: " + 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);
|
|
fontInstaller_.deleteFamily(family.name.c_str());
|
|
family.installed = false;
|
|
family.hasUpdate = false;
|
|
RenderLock lock(*this);
|
|
state_ = ERROR;
|
|
errorMessage_ = "Checksum mismatch: " + file.name;
|
|
return;
|
|
}
|
|
LOG_DBG("FONT", "Downloaded %s (size=%zu crc32=%08x)", file.name.c_str(), file.size, actualCrc);
|
|
|
|
if (!fontInstaller_.validateCpfontFile(destPath)) {
|
|
LOG_ERR("FONT", "Invalid .cpfont: %s", destPath);
|
|
fontInstaller_.deleteFamily(family.name.c_str());
|
|
family.installed = false;
|
|
family.hasUpdate = false;
|
|
RenderLock lock(*this);
|
|
state_ = ERROR;
|
|
errorMessage_ = "Invalid font file: " + file.name;
|
|
return;
|
|
}
|
|
}
|
|
|
|
fontInstaller_.refreshRegistry();
|
|
family.installed = true;
|
|
family.hasUpdate = false;
|
|
|
|
{
|
|
RenderLock lock(*this);
|
|
state_ = COMPLETE;
|
|
}
|
|
}
|
|
|
|
void FontDownloadActivity::promptDeleteSelectedFamily() {
|
|
const int pendingDeleteFamilyIndex = familyIndexFromList(selectedIndex_);
|
|
if (pendingDeleteFamilyIndex < 0 || pendingDeleteFamilyIndex >= static_cast<int>(families_.size())) {
|
|
return;
|
|
}
|
|
|
|
std::string heading = tr(STR_DELETE);
|
|
const auto& family = families_[pendingDeleteFamilyIndex];
|
|
std::string body = family.name;
|
|
startActivityForResult(std::make_unique<ConfirmationActivity>(renderer, mappedInput, heading, body),
|
|
[this](const ActivityResult& result) { onDeleteConfirmationResult(result); });
|
|
}
|
|
|
|
void FontDownloadActivity::onDeleteConfirmationResult(const ActivityResult& result) {
|
|
if (result.isCancelled) {
|
|
requestUpdate();
|
|
return;
|
|
}
|
|
|
|
auto& family = families_[familyIndexFromList(selectedIndex_)];
|
|
|
|
if (fontInstaller_.deleteFamily(family.name.c_str()) != FontInstaller::Error::OK) {
|
|
RenderLock lock(*this);
|
|
state_ = ERROR;
|
|
errorMessage_ = "Failed to delete font";
|
|
} else {
|
|
fontInstaller_.refreshRegistry();
|
|
family.installed = false;
|
|
family.hasUpdate = false;
|
|
}
|
|
|
|
requestUpdate();
|
|
}
|
|
|
|
bool FontDownloadActivity::isSelectedFamilyDeletable() const {
|
|
if (isDownloadAllRow(selectedIndex_) || isUpdateAllRow(selectedIndex_)) return false;
|
|
if (selectedIndex_ < specialRowCount() || selectedIndex_ >= listItemCount()) return false;
|
|
const auto& family = families_[familyIndexFromList(selectedIndex_)];
|
|
return family.installed && !family.hasUpdate;
|
|
}
|
|
|
|
// --- Input handling ---
|
|
|
|
void FontDownloadActivity::loop() {
|
|
if (state_ == FAMILY_LIST) {
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
|
finish();
|
|
return;
|
|
}
|
|
|
|
const int listSize = listItemCount();
|
|
const int pageItems = UITheme::getNumberOfItemsPerPage(renderer, true, false, true, false);
|
|
|
|
buttonNavigator_.onNextRelease([this, listSize] {
|
|
selectedIndex_ = ButtonNavigator::nextIndex(selectedIndex_, listSize);
|
|
requestUpdate();
|
|
});
|
|
|
|
buttonNavigator_.onPreviousRelease([this, listSize] {
|
|
selectedIndex_ = ButtonNavigator::previousIndex(selectedIndex_, listSize);
|
|
requestUpdate();
|
|
});
|
|
|
|
buttonNavigator_.onNextContinuous([this, listSize, pageItems] {
|
|
selectedIndex_ = ButtonNavigator::nextPageIndex(selectedIndex_, listSize, pageItems);
|
|
requestUpdate();
|
|
});
|
|
|
|
buttonNavigator_.onPreviousContinuous([this, listSize, pageItems] {
|
|
selectedIndex_ = ButtonNavigator::previousPageIndex(selectedIndex_, listSize, pageItems);
|
|
requestUpdate();
|
|
});
|
|
|
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
|
if (!families_.empty()) {
|
|
if (isDownloadAllRow(selectedIndex_)) {
|
|
downloadAll();
|
|
} else if (isUpdateAllRow(selectedIndex_)) {
|
|
updateAll();
|
|
} else {
|
|
auto& family = families_[familyIndexFromList(selectedIndex_)];
|
|
if (!family.installed || family.hasUpdate) {
|
|
downloadFamily(family);
|
|
} else {
|
|
promptDeleteSelectedFamily();
|
|
return;
|
|
}
|
|
}
|
|
requestUpdateAndWait();
|
|
return;
|
|
}
|
|
}
|
|
} 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())) {
|
|
downloadFamily(families_[downloadingFamilyIndex_]);
|
|
requestUpdateAndWait();
|
|
return;
|
|
} 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_BROWSER));
|
|
|
|
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) {
|
|
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 (isDownloadAllRow(index)) {
|
|
return std::string(tr(STR_DOWNLOAD_ALL)) + " (" + formatSize(totalDownloadSize()) + ")";
|
|
}
|
|
if (isUpdateAllRow(index)) {
|
|
return std::string(tr(STR_UPDATE_ALL)) + " (" + formatSize(totalUpdateSize()) + ")";
|
|
}
|
|
return families_[familyIndexFromList(index)].name;
|
|
},
|
|
[this](int index) -> std::string {
|
|
if (isDownloadAllRow(index) || isUpdateAllRow(index)) return "";
|
|
return families_[familyIndexFromList(index)].description;
|
|
},
|
|
nullptr,
|
|
[this](int index) -> std::string {
|
|
if (isDownloadAllRow(index) || isUpdateAllRow(index)) return "";
|
|
const auto& f = families_[familyIndexFromList(index)];
|
|
if (f.hasUpdate) return tr(STR_UPDATE_AVAILABLE);
|
|
if (f.installed) return tr(STR_INSTALLED);
|
|
return "";
|
|
},
|
|
true,
|
|
[this](int index) -> bool {
|
|
if (isDownloadAllRow(index) || isUpdateAllRow(index)) return false;
|
|
const auto& f = families_[familyIndexFromList(index)];
|
|
return f.installed && !f.hasUpdate;
|
|
});
|
|
|
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK),
|
|
isSelectedFamilyDeletable() ? tr(STR_DELETE)
|
|
: isUpdateAllRow(selectedIndex_) ? tr(STR_UPDATE)
|
|
: tr(STR_DOWNLOAD),
|
|
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);
|
|
} 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 auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_RETRY), "", "");
|
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
|
}
|
|
|
|
renderer.displayBuffer();
|
|
}
|