Merge pull request #207 from jpirnay/chore-fontmanager

chore: Font Manager  overhaul
This commit is contained in:
jpirnay
2026-05-13 14:15:48 +02:00
committed by GitHub
21 changed files with 256 additions and 47 deletions
+2 -1
View File
@@ -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!"
+2 -1
View File
@@ -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!"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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: "Шрифты недоступны"
+2 -1
View File
@@ -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!"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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!"
+2 -1
View File
@@ -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!"
+2 -1
View File
@@ -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: "Весь прогрес читання буде втрачено!"
+2 -1
View File
@@ -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!"
@@ -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();
@@ -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;
+127 -10
View File
@@ -60,6 +60,7 @@ void FontDownloadActivity::onWifiSelectionComplete(const bool success) {
RenderLock lock(*this);
state_ = FAMILY_LIST;
selectedIndex_ = 0;
previousActionCount_ = actionCount();
}
}
@@ -163,10 +164,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 +190,57 @@ 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;
}
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;
@@ -228,6 +287,8 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
currentFileIndex_ = i;
fileProgress_ = 0;
fileTotal_ = file.size;
lastProgressPercent_ = -1;
lastProgressUpdateMs_ = 0;
}
requestUpdateAndWait();
@@ -249,12 +310,36 @@ 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);
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());
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);
@@ -323,6 +408,7 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) {
fontInstaller_.refreshRegistry();
family.installed = true;
family.hasUpdate = false;
syncSelectedIndexForNewActionCount();
RenderLock lock(*this);
state_ = COMPLETE;
@@ -349,6 +435,7 @@ void FontDownloadActivity::deleteFamilyAtIndex(int familyIndex) {
fontInstaller_.refreshRegistry();
family.installed = false;
family.hasUpdate = false;
syncSelectedIndexForNewActionCount();
pendingErrorAction_ = PendingFontAction::None;
errorMessage_.clear();
@@ -377,6 +464,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);
@@ -387,6 +475,7 @@ std::string FontDownloadActivity::confirmButtonLabel() const {
void FontDownloadActivity::loop() {
if (state_ == FAMILY_LIST) {
syncSelectedIndexForNewActionCount();
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
finish();
return;
@@ -411,6 +500,9 @@ void FontDownloadActivity::loop() {
if (isDownloadAllSelected()) {
downloadAll();
requestUpdateAndWait();
} else if (isUpdateAllSelected()) {
updateAll();
requestUpdateAndWait();
} else {
const int familyIndex = familyIndexFromList(selectedIndex_);
const auto& family = families_[familyIndex];
@@ -488,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), "", "", "");
@@ -498,18 +591,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 +652,9 @@ void FontDownloadActivity::render(RenderLock&&) {
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), "", "", "");
+19 -3
View File
@@ -69,15 +69,31 @@ class FontDownloadActivity : public Activity {
int downloadingFamilyIndex_ = 0;
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();
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<int>(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() - 1 ? listIndex - actionCount() : -1;
}
int listItemCount() const { return families_.empty() ? 0 : static_cast<int>(families_.size()) + actionCount(); }
size_t totalUninstalledSize() const;
size_t totalUpdateSize() const;
void syncSelectedIndexForNewActionCount();
std::string confirmButtonLabel() const;
void promptDeleteFamily(int familyIndex);
+60 -14
View File
@@ -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<char*>(buffer), toRead);
if (readSize > 0) {
if (file.write(buffer, readSize) != static_cast<size_t>(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;
}
+1 -1
View File
@@ -10,7 +10,7 @@
*/
class HttpDownloader {
public:
using ProgressCallback = std::function<void(size_t downloaded, size_t total)>;
using ProgressCallback = std::function<bool(unsigned int downloaded, unsigned int total)>;
enum DownloadError {
OK = 0,