From d6fd744b3c15f2125042ff607efc5d253b602139 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Wed, 13 May 2026 12:39:24 +0200 Subject: [PATCH 1/2] Font Manager overhaul --- lib/I18n/translations/dutch.yaml | 3 +- lib/I18n/translations/english.yaml | 3 +- lib/I18n/translations/french.yaml | 3 +- lib/I18n/translations/german.yaml | 3 +- lib/I18n/translations/italian.yaml | 3 +- lib/I18n/translations/polish.yaml | 3 +- lib/I18n/translations/portuguese_br.yaml | 3 +- lib/I18n/translations/portuguese_pt.yaml | 3 +- lib/I18n/translations/russian.yaml | 3 +- lib/I18n/translations/slovenian.yaml | 3 +- lib/I18n/translations/spanish.yaml | 3 +- lib/I18n/translations/swedish.yaml | 3 +- lib/I18n/translations/turkish.yaml | 3 +- lib/I18n/translations/ukrainian.yaml | 3 +- lib/I18n/translations/vietnamese.yaml | 3 +- .../browser/OpdsBookBrowserActivity.cpp | 20 +++- .../browser/OpdsBookBrowserActivity.h | 3 +- .../settings/FontDownloadActivity.cpp | 93 +++++++++++++++++-- .../settings/FontDownloadActivity.h | 16 +++- src/network/HttpDownloader.cpp | 74 ++++++++++++--- src/network/HttpDownloader.h | 2 +- 21 files changed, 207 insertions(+), 46 deletions(-) diff --git a/lib/I18n/translations/dutch.yaml b/lib/I18n/translations/dutch.yaml index 026ef6b9..db303031 100644 --- a/lib/I18n/translations/dutch.yaml +++ b/lib/I18n/translations/dutch.yaml @@ -193,7 +193,8 @@ STR_REGISTER_FAILED: "Registratie mislukt" STR_USERNAME_TAKEN: "Gebruikersnaam al in gebruik" STR_DONE: "Klaar" STR_INSTALLED: "Geïnstalleerd" -STR_DOWNLOAD_ALL: "Alles downloaden / bijwerken" +STR_DOWNLOAD_ALL: "Alles downloaden" +STR_UPDATE_ALL: "Alles bijwerken" STR_UPDATE_AVAILABLE: "Update" STR_CLEAR_CACHE_WARNING_1: "Dit wist alle gecachte boekgegevens." STR_CLEAR_CACHE_WARNING_2: "Alle leesvoortgang gaat verloren!" diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index d7abbab9..75fa9da0 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -204,7 +204,8 @@ STR_REGISTER_FAILED: "Registration failed" STR_USERNAME_TAKEN: "Username already taken" STR_DONE: "Done" STR_INSTALLED: "Installed" -STR_DOWNLOAD_ALL: "Download / Update All" +STR_DOWNLOAD_ALL: "Download All" +STR_UPDATE_ALL: "Update All" STR_UPDATE_AVAILABLE: "Update" STR_CLEAR_CACHE_WARNING_1: "This will clear all cached book data." STR_CLEAR_CACHE_WARNING_2: "All reading progress will be lost!" diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index 2a05613d..4fb327f1 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -582,7 +582,8 @@ STR_KO_SYNC_ON_BOOK_CLOSE: "Envoi automatique à la fermeture du livre" STR_KO_AUTO_SYNC_SKIPPED: "Distant en avance, passage" STR_KO_LONGPRESS_HINT: "Maintenez Confirmer pour ouvrir avec la synchronisation" STR_INSTALLED: "Installé" -STR_DOWNLOAD_ALL: "Tout télécharger / mettre à jour" +STR_DOWNLOAD_ALL: "Tout télécharger" +STR_UPDATE_ALL: "Tout mettre à jour" STR_UPDATE_AVAILABLE: "Mettre à jour" STR_TINY: "Minuscule" STR_NO_FONTS_AVAILABLE: "Aucune police disponible" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index 1144d938..9c53354e 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -582,7 +582,8 @@ STR_KO_SYNC_ON_BOOK_CLOSE: "Automatischer Push bei Buchschließen" STR_KO_AUTO_SYNC_SKIPPED: "Remote voraus, überspringe" STR_KO_LONGPRESS_HINT: "Halte Bestätigen gedrückt, um mit Sync zu öffnen" STR_INSTALLED: "Installiert" -STR_DOWNLOAD_ALL: "Alle herunterladen/aktualisieren" +STR_DOWNLOAD_ALL: "Alle herunterladen" +STR_UPDATE_ALL: "Alle aktualisieren" STR_UPDATE_AVAILABLE: "Aktualisieren" STR_TINY: "Winzig" STR_NO_FONTS_AVAILABLE: "Keine Schriften verfügbar" diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index 6eec1151..516579f6 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -582,7 +582,8 @@ STR_KO_SYNC_ON_BOOK_CLOSE: "Invio automatico alla chiusura del libro" STR_KO_AUTO_SYNC_SKIPPED: "Remoto in anticipo, salto" STR_KO_LONGPRESS_HINT: "Tieni premuto Conferma per aprire con la sincronizzazione" STR_INSTALLED: "Installato" -STR_DOWNLOAD_ALL: "Scarica / aggiorna tutto" +STR_DOWNLOAD_ALL: "Scarica tutto" +STR_UPDATE_ALL: "Aggiorna tutto" STR_UPDATE_AVAILABLE: "Aggiorna" STR_TINY: "Minuscolo" STR_NO_FONTS_AVAILABLE: "Nessun font disponibile" diff --git a/lib/I18n/translations/polish.yaml b/lib/I18n/translations/polish.yaml index ca297cc2..ce64e544 100644 --- a/lib/I18n/translations/polish.yaml +++ b/lib/I18n/translations/polish.yaml @@ -427,7 +427,8 @@ STR_SD_CARD_FULL: "Za mało pamięci na karcie SD" STR_FILES_LABEL: "Pliki: " STR_SIZE_LABEL: "Rozmiar: " STR_REDOWNLOAD: "Pobierz ponownie" -STR_DOWNLOAD_ALL: "Pobierz / Uaktualnij wszystkie" +STR_DOWNLOAD_ALL: "Pobierz wszystkie" +STR_UPDATE_ALL: "Aktualizuj wszystko" STR_ALL_FONTS_INSTALLED: "Wszystkie czcionki zainstalowane!" STR_UPDATE_AVAILABLE: "Uaktualnij" STR_CRASH_TITLE: "Awaria systemu" diff --git a/lib/I18n/translations/portuguese_br.yaml b/lib/I18n/translations/portuguese_br.yaml index b4ebc4dd..aa1ceea8 100644 --- a/lib/I18n/translations/portuguese_br.yaml +++ b/lib/I18n/translations/portuguese_br.yaml @@ -577,7 +577,8 @@ STR_KO_SYNC_ON_BOOK_CLOSE: "Envio automático ao fechar o livro" STR_KO_AUTO_SYNC_SKIPPED: "Remoto à frente, pulando" STR_KO_LONGPRESS_HINT: "Segure Confirmar para abrir com sincronização" STR_INSTALLED: "Instalado" -STR_DOWNLOAD_ALL: "Baixar / atualizar tudo" +STR_DOWNLOAD_ALL: "Baixar tudo" +STR_UPDATE_ALL: "Atualizar tudo" STR_UPDATE_AVAILABLE: "Atualizar" STR_TINY: "Minúsculo" STR_NO_FONTS_AVAILABLE: "Nenhuma fonte disponível" diff --git a/lib/I18n/translations/portuguese_pt.yaml b/lib/I18n/translations/portuguese_pt.yaml index a4acc119..b9862f93 100644 --- a/lib/I18n/translations/portuguese_pt.yaml +++ b/lib/I18n/translations/portuguese_pt.yaml @@ -576,7 +576,8 @@ STR_KO_SYNC_ON_BOOK_CLOSE: "Envio automático ao fechar o livro" STR_KO_AUTO_SYNC_SKIPPED: "Remoto à frente, a ignorar" STR_KO_LONGPRESS_HINT: "Mantenha Confirmar para abrir com sincronização" STR_INSTALLED: "Instalado" -STR_DOWNLOAD_ALL: "Transferir / atualizar tudo" +STR_DOWNLOAD_ALL: "Transferir tudo" +STR_UPDATE_ALL: "Atualizar tudo" STR_UPDATE_AVAILABLE: "Atualizar" STR_TINY: "Minúsculo" STR_NO_FONTS_AVAILABLE: "Nenhuma fonte disponível" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index 7aaac9a5..57ab0c49 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -577,7 +577,8 @@ STR_KO_SYNC_ON_BOOK_CLOSE: "Авто-отправка при закрытии к STR_KO_AUTO_SYNC_SKIPPED: "Удаленный впереди, пропускаю" STR_KO_LONGPRESS_HINT: "Удерживайте Подтвердить, чтобы открыть с синхронизацией" STR_INSTALLED: "Установлено" -STR_DOWNLOAD_ALL: "Загрузить / обновить все" +STR_DOWNLOAD_ALL: "Загрузить все" +STR_UPDATE_ALL: "Обновить все" STR_UPDATE_AVAILABLE: "Обновить" STR_TINY: "Крошечный" STR_NO_FONTS_AVAILABLE: "Шрифты недоступны" diff --git a/lib/I18n/translations/slovenian.yaml b/lib/I18n/translations/slovenian.yaml index 572fbac6..81862896 100644 --- a/lib/I18n/translations/slovenian.yaml +++ b/lib/I18n/translations/slovenian.yaml @@ -193,7 +193,8 @@ STR_REGISTER_FAILED: "Registracija ni uspela" STR_USERNAME_TAKEN: "Uporabniško ime je že zasedeno" STR_DONE: "Končano" STR_INSTALLED: "Nameščeno" -STR_DOWNLOAD_ALL: "Prenesi / posodobi vse" +STR_DOWNLOAD_ALL: "Prenesi vse" +STR_UPDATE_ALL: "Posodobi vse" STR_UPDATE_AVAILABLE: "Posodobi" STR_CLEAR_CACHE_WARNING_1: "To bo izbrisalo vse predpomnjene podatke." STR_CLEAR_CACHE_WARNING_2: "Ves napredek branja bo izgubljen!" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index 07074a92..ee38f4a6 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -577,7 +577,8 @@ STR_KO_SYNC_ON_BOOK_CLOSE: "Empuje automático al cerrar el libro" STR_KO_AUTO_SYNC_SKIPPED: "Remoto adelantado, omitiendo" STR_KO_LONGPRESS_HINT: "Mantén pulsado Confirmar para abrir con sincronización" STR_INSTALLED: "Instalado" -STR_DOWNLOAD_ALL: "Descargar / actualizar todo" +STR_DOWNLOAD_ALL: "Descargar todo" +STR_UPDATE_ALL: "Actualizar todo" STR_UPDATE_AVAILABLE: "Actualizar" STR_TINY: "Minúsculo" STR_NO_FONTS_AVAILABLE: "No hay fuentes disponibles" diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index f8f7df07..88819e98 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -193,7 +193,8 @@ STR_REGISTER_FAILED: "Registrering misslyckades" STR_USERNAME_TAKEN: "Användarnamn upptaget" STR_DONE: "Klar" STR_INSTALLED: "Installerat" -STR_DOWNLOAD_ALL: "Ladda ner / uppdatera allt" +STR_DOWNLOAD_ALL: "Ladda ner allt" +STR_UPDATE_ALL: "Uppdatera allt" STR_UPDATE_AVAILABLE: "Uppdatera" STR_CLEAR_CACHE_WARNING_1: "Detta rensar all cachad bokdata" STR_CLEAR_CACHE_WARNING_2: "Alla läsframsteg kommer att försvinna!" diff --git a/lib/I18n/translations/turkish.yaml b/lib/I18n/translations/turkish.yaml index 0583d3cb..53036eed 100644 --- a/lib/I18n/translations/turkish.yaml +++ b/lib/I18n/translations/turkish.yaml @@ -193,7 +193,8 @@ STR_REGISTER_FAILED: "Kayıt başarısız" STR_USERNAME_TAKEN: "Kullanıcı adı zaten alınmış" STR_DONE: "Tamamlandı" STR_INSTALLED: "Yüklendi" -STR_DOWNLOAD_ALL: "Tümünü İndir / Güncelle" +STR_DOWNLOAD_ALL: "Tümünü indir" +STR_UPDATE_ALL: "Tümünü güncelle" STR_UPDATE_AVAILABLE: "Güncelle" STR_CLEAR_CACHE_WARNING_1: "Bu işlem tüm önbelleğe alınmış verileri siler." STR_CLEAR_CACHE_WARNING_2: "Tüm okuma ilerlemesi kaybolacak!" diff --git a/lib/I18n/translations/ukrainian.yaml b/lib/I18n/translations/ukrainian.yaml index 05969ae8..286883a6 100644 --- a/lib/I18n/translations/ukrainian.yaml +++ b/lib/I18n/translations/ukrainian.yaml @@ -193,7 +193,8 @@ STR_REGISTER_FAILED: "Помилка реєстрації" STR_USERNAME_TAKEN: "Ім'я користувача вже зайнято" STR_DONE: "Готово" STR_INSTALLED: "Встановлено" -STR_DOWNLOAD_ALL: "Завантажити / Оновити все" +STR_DOWNLOAD_ALL: "Завантажити все" +STR_UPDATE_ALL: "Оновити все" STR_UPDATE_AVAILABLE: "Оновити" STR_CLEAR_CACHE_WARNING_1: "Це очистить усі кешовані дані книг." STR_CLEAR_CACHE_WARNING_2: "Весь прогрес читання буде втрачено!" diff --git a/lib/I18n/translations/vietnamese.yaml b/lib/I18n/translations/vietnamese.yaml index 10c328d2..34178f8d 100644 --- a/lib/I18n/translations/vietnamese.yaml +++ b/lib/I18n/translations/vietnamese.yaml @@ -193,7 +193,8 @@ STR_REGISTER_FAILED: "Đăng ký thất bại" STR_USERNAME_TAKEN: "Tên đăng nhập đã được dùng" STR_DONE: "Xong" STR_INSTALLED: "Đã cài đặt" -STR_DOWNLOAD_ALL: "Tải xuống / Cập nhật tất cả" +STR_DOWNLOAD_ALL: "Tải xuống tất cả" +STR_UPDATE_ALL: "Cập nhật tất cả" STR_UPDATE_AVAILABLE: "Cập nhật" STR_CLEAR_CACHE_WARNING_1: "Thao tác này sẽ xóa toàn bộ dữ liệu sách đã lưu." STR_CLEAR_CACHE_WARNING_2: "Tất cả tiến trình đọc sẽ bị mất!" diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index 30482653..1fa25492 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -322,6 +322,8 @@ void OpdsBookBrowserActivity::render(RenderLock&&) { const int barY = midY + 20; GUI.drawProgressBar(renderer, Rect{barX, barY, barWidth, barHeight}, downloadProgress, downloadTotal); } + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); return; } @@ -535,10 +537,12 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book, const OpdsAcqu const auto result = HttpDownloader::downloadToFile( downloadUrl, filename, - [this](const size_t downloaded, const size_t total) { + [this](const unsigned int downloaded, const unsigned int total) { + mappedInput.update(); downloadProgress = downloaded; downloadTotal = total; requestUpdate(true); + return !mappedInput.wasPressed(MappedInputManager::Button::Back); }, server.username, server.password); @@ -583,8 +587,13 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book, const OpdsAcqu std::string sidecarPath = baseFilename + ext; const auto coverDlResult = HttpDownloader::downloadToFile( - coverUrl, sidecarPath, [this](const size_t, const size_t) { requestUpdate(true); }, server.username, - server.password); + coverUrl, sidecarPath, + [this](const unsigned int, const unsigned int) { + mappedInput.update(); + requestUpdate(true); + return !mappedInput.wasPressed(MappedInputManager::Button::Back); + }, + server.username, server.password); if (coverDlResult != HttpDownloader::OK) { LOG_ERR("OPDS", "Failed to download cover from %s (err %d)", coverUrl.c_str(), (int)coverDlResult); Storage.remove(sidecarPath.c_str()); @@ -600,6 +609,11 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book, const OpdsAcqu formatSelectionLabels.clear(); state = BrowserState::BROWSING; requestUpdate(); + } else if (result == HttpDownloader::ABORTED) { + selectedBookIndex = -1; + formatSelectionLabels.clear(); + state = BrowserState::BROWSING; + requestUpdate(); } else { selectedBookIndex = -1; formatSelectionLabels.clear(); diff --git a/src/activities/browser/OpdsBookBrowserActivity.h b/src/activities/browser/OpdsBookBrowserActivity.h index 107a2541..9ab24cbf 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.h +++ b/src/activities/browser/OpdsBookBrowserActivity.h @@ -42,7 +42,8 @@ class OpdsBookBrowserActivity final : public Activity { std::string currentPath; std::string searchTemplate; bool consumeConfirm = false; - bool consumeBack = false; // Added missing member + bool consumeBack = false; + bool cancelDownloadRequested = false; int selectorIndex = 0; int selectedBookIndex = -1; int formatSelectorIndex = 0; diff --git a/src/activities/settings/FontDownloadActivity.cpp b/src/activities/settings/FontDownloadActivity.cpp index f1ae7e43..db63609c 100644 --- a/src/activities/settings/FontDownloadActivity.cpp +++ b/src/activities/settings/FontDownloadActivity.cpp @@ -163,10 +163,23 @@ bool FontDownloadActivity::fetchAndParseManifest() { // --- Download --- void FontDownloadActivity::downloadAll() { + cancelRequested_ = false; for (size_t i = 0; i < families_.size(); i++) { - if (families_[i].installed && !families_[i].hasUpdate) continue; + if (families_[i].installed) continue; downloadFamily(families_[i]); - if (state_ == ERROR) return; + 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); @@ -176,12 +189,35 @@ void FontDownloadActivity::downloadAll() { size_t FontDownloadActivity::totalUninstalledSize() const { size_t total = 0; for (const auto& f : families_) { - if (!f.installed || f.hasUpdate) total += f.totalSize; + 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; +} + +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; @@ -249,12 +285,23 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) { std::string url = baseUrl_ + file.name; - auto result = HttpDownloader::downloadToFile(url, stagedPath, [this](size_t downloaded, size_t total) { + auto result = HttpDownloader::downloadToFile(url, stagedPath, [this](unsigned int downloaded, unsigned int total) { + mappedInput.update(); fileProgress_ = downloaded; fileTotal_ = total; requestUpdate(true); + return !mappedInput.wasPressed(MappedInputManager::Button::Back); }); + if (result == HttpDownloader::ABORTED) { + LOG_INF("FONT", "Download cancelled: %s", file.name.c_str()); + Storage.removeDir(stagingDir); + 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); Storage.removeDir(stagingDir); @@ -377,6 +424,7 @@ void FontDownloadActivity::deleteFamilyAtIndex(int familyIndex) { 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); @@ -411,6 +459,9 @@ void FontDownloadActivity::loop() { if (isDownloadAllSelected()) { downloadAll(); requestUpdateAndWait(); + } else if (isUpdateAllSelected()) { + updateAll(); + requestUpdateAndWait(); } else { const int familyIndex = familyIndexFromList(selectedIndex_); const auto& family = families_[familyIndex]; @@ -498,18 +549,39 @@ void FontDownloadActivity::render(RenderLock&&) { Rect{0, contentTop, pageWidth, pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing}, listItemCount(), selectedIndex_, [this](int index) -> std::string { - if (index == 0) { - return std::string(tr(STR_DOWNLOAD_ALL)) + " (" + formatSize(totalUninstalledSize()) + ")"; + 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; }, - nullptr, nullptr, [this](int index) -> std::string { - if (index == 0) return ""; + 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); - return f.description; + return ""; }, true); @@ -538,6 +610,9 @@ void FontDownloadActivity::render(RenderLock&&) { int percentY = barY + metrics.progressBarHeight + metrics.verticalSpacing; renderer.drawCenteredText(UI_10_FONT_ID, percentY, (std::to_string(static_cast(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), "", "", ""); diff --git a/src/activities/settings/FontDownloadActivity.h b/src/activities/settings/FontDownloadActivity.h index 62316ce5..1495a72f 100644 --- a/src/activities/settings/FontDownloadActivity.h +++ b/src/activities/settings/FontDownloadActivity.h @@ -69,15 +69,25 @@ class FontDownloadActivity : public Activity { int downloadingFamilyIndex_ = 0; PendingFontAction pendingErrorAction_ = PendingFontAction::None; std::string errorMessage_; + bool cancelRequested_ = false; void onWifiSelectionComplete(bool success); bool fetchAndParseManifest(); void downloadFamily(ManifestFamily& family); void downloadAll(); - bool isDownloadAllSelected() const { return selectedIndex_ == 0 && !families_.empty(); } - int familyIndexFromList(int listIndex) const { return listIndex - 1; } - int listItemCount() const { return families_.empty() ? 0 : static_cast(families_.size()) + 1; } + void updateAll(); + bool isDownloadAllSelected() const { return hasDownloadCandidates() && selectedIndex_ == 0; } + bool isUpdateAllSelected() const { + if (!hasUpdateCandidates()) return false; + return selectedIndex_ == (hasDownloadCandidates() ? 1 : 0); + } + bool hasDownloadCandidates() const; + bool hasUpdateCandidates() const; + int actionCount() const { return (hasDownloadCandidates() ? 1 : 0) + (hasUpdateCandidates() ? 1 : 0); } + int familyIndexFromList(int listIndex) const { return listIndex - actionCount(); } + int listItemCount() const { return families_.empty() ? 0 : static_cast(families_.size()) + actionCount(); } size_t totalUninstalledSize() const; + size_t totalUpdateSize() const; std::string confirmButtonLabel() const; void promptDeleteFamily(int familyIndex); diff --git a/src/network/HttpDownloader.cpp b/src/network/HttpDownloader.cpp index d8b15a22..be044b52 100644 --- a/src/network/HttpDownloader.cpp +++ b/src/network/HttpDownloader.cpp @@ -17,7 +17,7 @@ namespace { class FileWriteStream final : public Stream { public: FileWriteStream(FsFile& file, size_t total, HttpDownloader::ProgressCallback progress) - : file_(file), total_(total), progress_(std::move(progress)) {} + : file_(file), total_(total), progress_(std::move(progress)), abortRequested_(false) {} size_t write(uint8_t byte) override { return write(&byte, 1); } @@ -28,8 +28,11 @@ class FileWriteStream final : public Stream { writeOk_ = false; } downloaded_ += written; - if (progress_ && total_ > 0) { - progress_(downloaded_, total_); + if (progress_) { + if (!progress_(downloaded_, total_)) { + abortRequested_ = true; + return 0; + } } return written; } @@ -41,12 +44,14 @@ class FileWriteStream final : public Stream { size_t downloaded() const { return downloaded_; } bool ok() const { return writeOk_; } + bool aborted() const { return abortRequested_; } private: FsFile& file_; size_t total_; size_t downloaded_ = 0; bool writeOk_ = true; + bool abortRequested_ = false; HttpDownloader::ProgressCallback progress_; }; } // namespace @@ -126,7 +131,7 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& http.begin(*client, url.c_str()); http.setReuse(false); http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS); - http.setTimeout(65535); // max uint16_t (~65s) — HTTPClient::setTimeout takes uint16_t ms + http.setTimeout(30000); http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION); http.addHeader("Connection", "close"); @@ -160,23 +165,26 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& // Open file for writing FsFile file; if (!Storage.openFileForWrite("HTTP", destPath.c_str(), file)) { - LOG_ERR("HTTP", "Failed to open file for writing"); + LOG_ERR("HTTP", "Failed to open file for writing: %s", destPath.c_str()); http.end(); return FILE_ERROR; } + LOG_DBG("HTTP", "Opened destination file for writing: %s", destPath.c_str()); + int writeResult = -1; size_t downloaded = 0; bool writeOk = true; - // Let HTTPClient handle chunked decoding and stream body bytes into the file. - // For known sizes (Content-Length), we can stream it manually to save RAM! - // HTTPClient::writeToStream allocates a 4096-byte chunk on the heap which can - // fail (-8 / HTTPC_ERROR_TOO_LESS_RAM) if the heap is fragmented or depleted. if (contentLength > 0) { NetworkClient& stream = http.getStream(); uint8_t buffer[1024]; writeResult = 1; + bool aborted = false; + unsigned long lastAvailLog = millis(); + unsigned long startMs = millis(); + unsigned long lastProgressPoll = millis(); + while (http.connected() && downloaded < contentLength) { size_t available = stream.available(); if (available > 0) { @@ -184,31 +192,69 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& if (downloaded + toRead > contentLength) { toRead = contentLength - downloaded; } - int readSize = stream.readBytes(buffer, toRead); + int readSize = stream.readBytes(reinterpret_cast(buffer), toRead); if (readSize > 0) { if (file.write(buffer, readSize) != static_cast(readSize)) { + LOG_ERR("HTTP", "File write failed: wrote %d/%zu bytes to %s", readSize, toRead, destPath.c_str()); writeOk = false; writeResult = -1; break; } downloaded += readSize; - if (progress) progress(downloaded, contentLength); + if (progress && !progress(downloaded, contentLength)) { + LOG_DBG("HTTP", "Download aborted by callback at %zu/%zu", downloaded, contentLength); + aborted = true; + break; + } } else { + LOG_ERR("HTTP", "Stream readBytes returned %d after %zu bytes", readSize, downloaded); break; } } else { + if (millis() - lastProgressPoll > 100) { + if (progress && !progress(downloaded, contentLength)) { + LOG_DBG("HTTP", "Download aborted by callback while waiting for data at %zu/%zu", downloaded, + contentLength); + aborted = true; + break; + } + lastProgressPoll = millis(); + } + if (millis() - lastAvailLog > 2000) { + LOG_DBG("HTTP", "Waiting for available data: downloaded=%zu connected=%d elapsed=%lums", downloaded, + http.connected(), millis() - startMs); + lastAvailLog = millis(); + } delay(1); } } + + if (aborted) { + file.flush(); + file.close(); + http.end(); + client->stop(); + Storage.remove(destPath.c_str()); + return ABORTED; + } + if (downloaded != contentLength) { + LOG_ERR("HTTP", "Download size mismatch after loop: got %zu expected %zu", downloaded, contentLength); writeResult = -1; } } else { - // Chunked or unknown length fallback FileWriteStream fileStream(file, contentLength, progress); writeResult = http.writeToStream(&fileStream); downloaded = fileStream.downloaded(); writeOk = fileStream.ok(); + if (fileStream.aborted()) { + file.flush(); + file.close(); + http.end(); + client->stop(); + Storage.remove(destPath.c_str()); + return ABORTED; + } } // Flush before closing to ensure data is written to the SD card. @@ -220,7 +266,7 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& client->stop(); if (writeResult < 0) { - LOG_ERR("HTTP", "writeToStream error: %d", writeResult); + LOG_ERR("HTTP", "writeToStream error: %d (downloaded %zu)", writeResult, downloaded); Storage.remove(destPath.c_str()); return HTTP_ERROR; } @@ -229,7 +275,7 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& // Guard against partial writes even if HTTPClient completes. if (!writeOk) { - LOG_ERR("HTTP", "Write failed during download"); + LOG_ERR("HTTP", "Write failed during download (downloaded %zu)", downloaded); Storage.remove(destPath.c_str()); return FILE_ERROR; } diff --git a/src/network/HttpDownloader.h b/src/network/HttpDownloader.h index 216840a5..5129549c 100644 --- a/src/network/HttpDownloader.h +++ b/src/network/HttpDownloader.h @@ -10,7 +10,7 @@ */ class HttpDownloader { public: - using ProgressCallback = std::function; + using ProgressCallback = std::function; enum DownloadError { OK = 0, From cc05a7eb006d242d5041e08b52684528cef0389e Mon Sep 17 00:00:00 2001 From: jpirnay Date: Wed, 13 May 2026 14:08:25 +0200 Subject: [PATCH 2/2] Reduce updates + review comments --- .../settings/FontDownloadActivity.cpp | 44 ++++++++++++++++++- .../settings/FontDownloadActivity.h | 8 +++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/activities/settings/FontDownloadActivity.cpp b/src/activities/settings/FontDownloadActivity.cpp index db63609c..dad2a6a4 100644 --- a/src/activities/settings/FontDownloadActivity.cpp +++ b/src/activities/settings/FontDownloadActivity.cpp @@ -60,6 +60,7 @@ void FontDownloadActivity::onWifiSelectionComplete(const bool success) { RenderLock lock(*this); state_ = FAMILY_LIST; selectedIndex_ = 0; + previousActionCount_ = actionCount(); } } @@ -202,6 +203,28 @@ size_t FontDownloadActivity::totalUpdateSize() const { 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; @@ -264,6 +287,8 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) { currentFileIndex_ = i; fileProgress_ = 0; fileTotal_ = file.size; + lastProgressPercent_ = -1; + lastProgressUpdateMs_ = 0; } requestUpdateAndWait(); @@ -289,7 +314,20 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) { mappedInput.update(); fileProgress_ = downloaded; fileTotal_ = total; - requestUpdate(true); + + const unsigned long now = millis(); + int percent = 0; + if (total > 0) { + percent = static_cast((static_cast(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); }); @@ -370,6 +408,7 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) { fontInstaller_.refreshRegistry(); family.installed = true; family.hasUpdate = false; + syncSelectedIndexForNewActionCount(); RenderLock lock(*this); state_ = COMPLETE; @@ -396,6 +435,7 @@ void FontDownloadActivity::deleteFamilyAtIndex(int familyIndex) { fontInstaller_.refreshRegistry(); family.installed = false; family.hasUpdate = false; + syncSelectedIndexForNewActionCount(); pendingErrorAction_ = PendingFontAction::None; errorMessage_.clear(); @@ -435,6 +475,7 @@ std::string FontDownloadActivity::confirmButtonLabel() const { void FontDownloadActivity::loop() { if (state_ == FAMILY_LIST) { + syncSelectedIndexForNewActionCount(); if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { finish(); return; @@ -539,6 +580,7 @@ void FontDownloadActivity::render(RenderLock&&) { 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), "", "", ""); diff --git a/src/activities/settings/FontDownloadActivity.h b/src/activities/settings/FontDownloadActivity.h index 1495a72f..dfe5f5f3 100644 --- a/src/activities/settings/FontDownloadActivity.h +++ b/src/activities/settings/FontDownloadActivity.h @@ -70,6 +70,9 @@ class FontDownloadActivity : public Activity { PendingFontAction pendingErrorAction_ = PendingFontAction::None; std::string errorMessage_; bool cancelRequested_ = false; + int previousActionCount_ = 0; + int lastProgressPercent_ = -1; + unsigned long lastProgressUpdateMs_ = 0; void onWifiSelectionComplete(bool success); bool fetchAndParseManifest(); @@ -84,10 +87,13 @@ class FontDownloadActivity : public Activity { bool hasDownloadCandidates() const; bool hasUpdateCandidates() const; int actionCount() const { return (hasDownloadCandidates() ? 1 : 0) + (hasUpdateCandidates() ? 1 : 0); } - int familyIndexFromList(int listIndex) const { return listIndex - actionCount(); } + int familyIndexFromList(int listIndex) const { + return listIndex > actionCount() - 1 ? listIndex - actionCount() : -1; + } int listItemCount() const { return families_.empty() ? 0 : static_cast(families_.size()) + actionCount(); } size_t totalUninstalledSize() const; size_t totalUpdateSize() const; + void syncSelectedIndexForNewActionCount(); std::string confirmButtonLabel() const; void promptDeleteFamily(int familyIndex);