From 413cae80c4c3ca5f078e3ea69e81283ac1dd8cf1 Mon Sep 17 00:00:00 2001 From: Gres Date: Thu, 8 Apr 2021 23:11:21 +0300 Subject: [PATCH] Updates refactoring Install updates immediately after click Separate loader and installer logic --- share/translations/screentranslator_ru.ts | 300 +++--- src/manager.cpp | 24 +- src/manager.h | 3 +- src/service/updates.cpp | 1143 +++++++++------------ src/service/updates.h | 147 ++- src/settingseditor.cpp | 15 +- src/settingseditor.h | 4 +- src/settingseditor.ui | 45 +- src/stfwd.h | 3 +- tests/updates_test.cpp | 87 +- 10 files changed, 698 insertions(+), 1073 deletions(-) diff --git a/share/translations/screentranslator_ru.ts b/share/translations/screentranslator_ru.ts index f23bf7d..680e629 100644 --- a/share/translations/screentranslator_ru.ts +++ b/share/translations/screentranslator_ru.ts @@ -616,74 +616,69 @@ Ctrl - продолжить выделять Любой язык - + app приложение - + recognizers распознавание - + correction коррекция - + translators перевод - + Screen translator started Экранный переводчик запущен - - Update completed - Обновление завершено - - - + Updates available Доступны обновления - + Current version might be outdated. Check for updates to silence this warning Текущая версия может быть устаревшей. Проверьте обновления, чтобы отключить это сообщение - + No recognition languages available. Install some via Settings->Updates Нет доступных языков распознавания. Установите нужные через Настройки->Обновление - + Recognition language not set. Go to Settings->Recognition and set it Язык распознавания не задан. Задайте его в Настройки->Распознавание - + No translators enabled. Go to Settings->Translation and select some Не выбран ни один переводчик. Активируйте хотя бы один в Настройки->Перевод - + Translation language not set. Go to Settings->Translation and set it Язык перевода не задан. Задайте его в Настройки->Перевод - + Failed to set log file: %1 Ошибка установки лог-файла: %1 - + Started logging to file: %1 Начата запись в лог-файл: %1 @@ -732,32 +727,32 @@ in %1 Не восстанавливать интерфейс пользователя (размер и положения окна и т.д.) - + <p>Optical character recognition (OCR) and translation tool</p> <p>Инструмент оптического распознавания текста (OCR) и перевода</p> - + <p>Version: %1</p> <p>Версия: %1</p> - + <p>Changelog: <a href="%1">%2</a></p> <p>Список изменений: <a href="%1">%2</a></p> - + <p>License: <a href="%3">MIT</a></p> <p>Лицензия: <a href="%3">MIT</a></p> - + <p>Author: Gres (<a href="mailto:%1">%1</a>)</p> <p>Автор: Gres (<a href="mailto:%1">%1</a>)</p> - + <p>Issues: <a href="%1">%1</a></p> <p>Поддержка: <a href="%1">%1</a></p> @@ -1034,20 +1029,15 @@ in %1 Показывать распознанное - + Update check interval (days): Интервал проверки обновления (дней): - + 0 - disabled - отключено - - - Apply updates - Применить изменения - Translate text @@ -1074,7 +1064,7 @@ in %1 Окно - + Check now Проверить сейчас @@ -1159,7 +1149,7 @@ in %1 Текст для проверки - + The program workflow consists of the following steps: 1. Selection on the screen area 2. Recognition of the selected area @@ -1182,7 +1172,7 @@ Then set default recognition and translation languages, enable some (or all) tra Далее установите языки распознавания и перевода по умолчанию, активируйте некоторые (или все) переводчики и настройку "переводить текст", если нужно. - + Portable changed. Apply settings first Portable режиме изменени. Сначала применить настройки @@ -1310,73 +1300,94 @@ Most likely they are already in use by another program Updates - + Tb Тб - + Gb Гб - + Mb Мб - + Kb Кб - + bytes байт - + Not installed Не установлено - + Update available Доступно обновление - + Up to date Последняя версия - + Remove Удалить - + Install/Update Установить/Обновить - - + Directory is not writable %1 Папка недоступна для записи %1 - - Downloaded file not exists -%1 - Скачанный файл не существует -%1 + + Failed to create temp file +%1 +Error %2 + Ошибка создания временного файла +%1 +%2 - - + + Failed to write to temp file +%1 +Error %2 + Ошибка записи во временный файл +%1 +%2 + + + + Failed to copy file +%1 +to %2 +Error %3 + Ошибка копирования файла +%1 +в %2 +%3 + + + + Failed to remove file %1 Error %2 @@ -1385,23 +1396,12 @@ Error %2 Текст %2 - + Failed to create path %1 Ошибка создания папки %1 - - - Failed to move file -%1 -to %2 -Error %3 - Ошибка переименования файла -%1 -в %2 -Текст %3 - WebPage @@ -1414,142 +1414,76 @@ Error %3 update::Loader - - Empty updates info from -%1 - Пустой файл обновлений из -%1 - - - - Empty updates info unpacked from -%1 - Пустой файл обновлений распакован из -%1 - - - - Failed to parse updates from -%1 (%2) - Ошибка разбора файла обновлений -%1 (%2) - - - + Failed to download file %1. Error %2 Ошибка скачивания файла %1. Текст %2 - - Already updating - Обновление в процессе - - - - No actions selected - Нет выделенных действий - - - - Failed to create temp path -%1 - Ошибка создания временной папки -%1 - - - + Empty data downloaded from %1 Пустой файл из %1 + + + update::Model - + + Failed to parse: %1 at %2 + Ошибка разбора: %1 в %2 + + + + Wrong updates version: %1 + Неверная версия файла обновлений: %1 + + + + No data parsed + Нет разобранных данных + + + + Name + Название + + + + State + Состояние + + + + Size + Размер + + + + Version + Версия + + + + Progress + Прогресс + + + + update::Updater + + Empty data unpacked from %1 Пустой файл распакован из %1 - - Failed to save downloaded file -%1 -to %2 -Error %3 - Ошибка сохранения скачанного файла -%1 -в %2 -Текст %3 - - - - Update failed: %1 - Ошибка обновления: %1 - - - - update::Model - - - Select all updates - Выбрать все обновления - - - - Reset actions - Сбросить действия - - - - Failed to parse: %1 at %2 - Ошибка разбора: %1 в %2 - - - - Wrong updates version: %1 - Неверная версия файла обновлений: %1 - - - - No data parsed - Нет разобранных данных - - - - Name - Название - - - - State - Состояние - - - - Action - Действие - - - - Size - Размер - - - - Version - Версия - - - - Progress - Прогресс - - - - Files - Файла + + Update all + Обновить все diff --git a/src/manager.cpp b/src/manager.cpp index 219ee2a..6df8547 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -33,8 +33,7 @@ using Loader = update::Loader; Manager::Manager() : models_(std::make_unique()) , settings_(std::make_unique()) - , updater_(std::make_unique(Loader::Urls{{updatesUrl}})) - , updateAutoChecker_(std::make_unique(*updater_)) + , updater_(std::make_unique(QVector{{updatesUrl}})) { SOFT_ASSERT(settings_, return ); @@ -61,13 +60,9 @@ Manager::Manager() warnIfOutdated(); - QObject::connect(updater_.get(), &update::Loader::error, // + QObject::connect(updater_.get(), &update::Updater::error, // tray_.get(), &TrayIcon::showError); - QObject::connect(updater_.get(), &update::Loader::updated, // - tray_.get(), [this] { - tray_->showInformation(QObject::tr("Update completed")); - }); - QObject::connect(updater_.get(), &update::Loader::updatesAvailable, // + QObject::connect(updater_.get(), &update::Updater::updatesAvailable, // tray_.get(), [this] { tray_->showInformation(QObject::tr("Updates available")); }); @@ -76,8 +71,10 @@ Manager::Manager() Manager::~Manager() { SOFT_ASSERT(settings_, return ); - if (updateAutoChecker_ && updateAutoChecker_->isLastCheckDateChanged()) { - settings_->lastUpdateCheck = updateAutoChecker_->lastCheckDate(); + SOFT_ASSERT(updater_, return ); + if (updater_->lastUpdateCheck().isValid() && + settings_->lastUpdateCheck != updater_->lastUpdateCheck()) { + settings_->lastUpdateCheck = updater_->lastUpdateCheck(); settings_->saveLastUpdateCheck(); LTRACE() << "saved last update time"; } @@ -170,16 +167,15 @@ void Manager::setupProxy(const Settings &settings) void Manager::setupUpdates(const Settings &settings) { - updater_->model()->setExpansions({ + updater_->setExpansions({ {"$translators$", settings.translatorsDir}, {"$tessdata$", settings.tessdataPath}, {"$hunspell$", settings.hunspellDir}, {"$appdir$", QApplication::applicationDirPath()}, }); - SOFT_ASSERT(updateAutoChecker_, return ); - updateAutoChecker_->setLastCheckDate(settings.lastUpdateCheck); - updateAutoChecker_->setCheckIntervalDays(settings.autoUpdateIntervalDays); + updater_->setAutoUpdate(settings.autoUpdateIntervalDays, + settings.lastUpdateCheck); } bool Manager::setupTrace(bool isOn) diff --git a/src/manager.h b/src/manager.h index 340c7d3..020a741 100644 --- a/src/manager.h +++ b/src/manager.h @@ -43,7 +43,6 @@ private: std::unique_ptr corrector_; std::unique_ptr translator_; std::unique_ptr representer_; - std::unique_ptr updater_; - std::unique_ptr updateAutoChecker_; + std::unique_ptr updater_; int activeTaskCount_{0}; }; diff --git a/src/service/updates.cpp b/src/service/updates.cpp index 526b6a9..f609a3c 100644 --- a/src/service/updates.cpp +++ b/src/service/updates.cpp @@ -1,9 +1,7 @@ #include "updates.h" #include "debug.h" -#include #include -#include #include #include #include @@ -12,7 +10,7 @@ #include #include #include -#include +#include #include #include @@ -121,7 +119,6 @@ QString toString(State state) QString toString(Action action) { const QMap names{ - {Action::NoAction, {}}, {Action::Remove, QApplication::translate("Updates", "Remove")}, {Action::Install, QApplication::translate("Updates", "Install/Update")}, }; @@ -146,349 +143,11 @@ QStringList toList(const QJsonValue &value) } // namespace -Loader::Loader(const update::Loader::Urls &updateUrls, QObject *parent) - : QObject(parent) - , network_(new QNetworkAccessManager(this)) - , model_(new Model(this)) - , updateUrls_(updateUrls) - , downloadPath_( - QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + - "/updates") +// + +Model::Model(Updater &updater) + : updater_(updater) { - std::random_device device; - std::mt19937 generator(device()); - std::shuffle(updateUrls_.begin(), updateUrls_.end(), generator); - - network_->setRedirectPolicy( - QNetworkRequest::RedirectPolicy::NoLessSafeRedirectPolicy); - connect(network_, &QNetworkAccessManager::finished, // - this, &Loader::handleReply); -} - -void Loader::handleReply(QNetworkReply *reply) -{ - if (updateUrls_.contains(reply->url())) { - handleUpdateReply(reply); - } else { - handleComponentReply(reply); - } -} - -void Loader::checkForUpdates() -{ - LTRACE() << "Loader::checkForUpdates"; - startDownloadUpdates({}); -} - -void Loader::startDownloadUpdates(const QUrl &previous) -{ - SOFT_ASSERT(!updateUrls_.isEmpty(), return ); - - QUrl url; - if (previous.isEmpty()) - url = updateUrls_.first(); - else { - const auto index = updateUrls_.indexOf(previous); - SOFT_ASSERT(index != -1, return ); - if (index + 1 < updateUrls_.size()) - url = updateUrls_[index + 1]; - } - - if (url.isEmpty()) { - dumpErrors(); - return; - } - - auto reply = network_->get(QNetworkRequest(url)); - if (reply->error() != QNetworkReply::NoError) - handleUpdateReply(reply); -} - -void Loader::handleUpdateReply(QNetworkReply *reply) -{ - reply->deleteLater(); - - const auto url = reply->url(); - if (reply->error() != QNetworkReply::NoError) { - addError(toError(*reply)); - startDownloadUpdates(url); - return; - } - - const auto replyData = reply->readAll(); - if (replyData.isEmpty()) { - addError(tr("Empty updates info from\n%1").arg(url.toString())); - startDownloadUpdates(url); - return; - } - - const auto unpacked = - url.toString().endsWith(".zip") ? unpack(replyData) : replyData; - - if (unpacked.isEmpty()) { - addError(tr("Empty updates info unpacked from\n%1").arg(url.toString())); - startDownloadUpdates(url); - return; - } - - SOFT_ASSERT(model_, return ); - const auto parseError = model_->parse(unpacked); - if (!parseError.isEmpty()) { - addError(tr("Failed to parse updates from\n%1 (%2)") - .arg(url.toString(), parseError)); - startDownloadUpdates(url); - return; - } - - errors_.clear(); - - if (model_->hasUpdates()) - emit updatesAvailable(); -} - -QString Loader::toError(QNetworkReply &reply) const -{ - return tr("Failed to download file\n%1. Error %2") - .arg(reply.url().toString(), reply.errorString()); -} - -void Loader::applyUserActions() -{ - SOFT_ASSERT(model_, return ); - if (!currentActions_.empty() || !downloads_.empty()) { - emit error(tr("Already updating")); - return; - } - - currentActions_ = model_->userActions(); - if (currentActions_.empty()) { - emit error(tr("No actions selected")); - return; - } - - for (auto &action : currentActions_) { - if (action.first != Action::Install) - continue; - - auto &file = action.second; - file.downloadPath = downloadPath_ + '/' + file.rawPath; - - if (!startDownload(file)) { - finishUpdate(); - return; - } - } - - if (downloads_.empty()) // no downloads - commitUpdate(); -} - -bool Loader::startDownload(File &file) -{ - if (file.urls.empty()) - return false; - - auto reply = network_->get(QNetworkRequest(file.urls.takeFirst())); - downloads_.emplace(reply, &file); - - connect(reply, &QNetworkReply::downloadProgress, // - this, &Loader::updateProgress); - - if (reply->error() == QNetworkReply::NoError) - return true; - - return handleComponentReply(reply); -} - -bool Loader::handleComponentReply(QNetworkReply *reply) -{ - reply->deleteLater(); - - if (currentActions_.empty()) { // aborted? - finishUpdate(); - return false; - } - - SOFT_ASSERT(downloads_.count(reply) == 1, return false); - auto *file = downloads_[reply]; - SOFT_ASSERT(file, return false); - - downloads_.erase(reply); - - if (reply->error() != QNetworkReply::NoError) { - addError(toError(*reply)); - - if (!startDownload(*file)) - finishUpdate(); - - return false; - } - - const auto &fileName = file->downloadPath; - auto dir = QFileInfo(fileName).absoluteDir(); - if (!dir.exists() && !dir.mkpath(".")) { - finishUpdate(tr("Failed to create temp path\n%1").arg(dir.absolutePath())); - return false; - } - - const auto url = reply->url(); - const auto replyData = reply->readAll(); - if (replyData.isEmpty()) { - addError(tr("Empty data downloaded from\n%1").arg(url.toString())); - - if (!startDownload(*file)) - finishUpdate(); - - return false; - } - - const auto mustUnpack = - url.toString().endsWith(".zip") && !fileName.endsWith(".zip"); - const auto unpacked = mustUnpack ? unpack(replyData) : replyData; - - if (unpacked.isEmpty()) { - addError(tr("Empty data unpacked from\n%1").arg(url.toString())); - - if (!startDownload(*file)) - finishUpdate(); - - return false; - } - - QFile f(fileName); - if (!f.open(QFile::WriteOnly)) { - const auto error = tr("Failed to save downloaded file\n%1\nto %2\nError %3") - .arg(url.toString(), f.fileName(), f.errorString()); - finishUpdate(error); - return false; - } - - f.write(unpacked); - f.close(); - - if (downloads_.empty()) - commitUpdate(); - - return true; -} - -void Loader::finishUpdate(const QString &error) -{ - LTRACE() << "Loader::finishUpdate"; - currentActions_.clear(); - for (const auto &i : downloads_) i.first->deleteLater(); - downloads_.clear(); - if (!error.isEmpty()) - addError(error); - dumpErrors(); - SOFT_ASSERT(model_, return ); - model_->updateStates(); -} - -void Loader::commitUpdate() -{ - LTRACE() << "Loader::commitUpdate"; - SOFT_ASSERT(!currentActions_.empty(), return ); - Installer installer(currentActions_); - if (installer.commit()) { - model_->resetProgress(); - errors_.clear(); - emit updated(); - } else { - addError(tr("Update failed: %1").arg(installer.errorString())); - } - finishUpdate(); -} - -void Loader::updateProgress(qint64 bytesSent, qint64 bytesTotal) -{ - if (bytesTotal < 1) - return; - const auto reply = qobject_cast(sender()); - SOFT_ASSERT(reply, return ); - const auto progress = int(100.0 * bytesSent / bytesTotal); - model_->updateProgress(reply->url(), progress); -} - -Model *Loader::model() const -{ - return model_; -} - -void Loader::addError(const QString &text) -{ - LTRACE() << text; - errors_.append(text); -} - -void Loader::dumpErrors() -{ - if (errors_.isEmpty()) - return; - const auto summary = errors_.join('\n'); - emit error(summary); - errors_.clear(); -} - -Model::Model(QObject *parent) - : QAbstractItemModel(parent) -{ -} - -void Model::initView(QTreeView *view) -{ - view->setSelectionMode(QAbstractItemView::ExtendedSelection); - auto proxy = new QSortFilterProxyModel(view); - proxy->setSourceModel(this); - view->setModel(proxy); - view->setItemDelegate(new update::UpdateDelegate(view)); - view->setSortingEnabled(true); - view->sortByColumn(int(Column::Name), Qt::AscendingOrder); -#ifndef DEVELOP - view->hideColumn(int(update::Model::Column::Files)); -#endif - - view->setContextMenuPolicy(Qt::CustomContextMenu); - connect(view, &QAbstractItemView::customContextMenuRequested, // - this, [this, view, proxy] { - QMenu menu; - using A = Action; - QMap actions; - for (auto i : QVector{A::Install, A::Remove, A::NoAction}) - actions[menu.addAction(toString(i))] = i; - menu.addSeparator(); - auto updateAll = menu.addAction(tr("Select all updates")); - auto reset = menu.addAction(tr("Reset actions")); - - const auto menuItem = menu.exec(QCursor::pos()); - if (!menuItem) - return; - - if (menuItem == updateAll) { - selectAllUpdates(); - return; - } - - if (menuItem == reset) { - resetActions(); - return; - } - - const auto selection = view->selectionModel(); - SOFT_ASSERT(selection, return ); - const auto indexes = selection->selectedRows(int(Column::Action)); - if (indexes.isEmpty()) - return; - - const auto action = actions[menuItem]; - - for (const auto &proxyIndex : indexes) { - auto modelIndex = proxy->mapToSource(proxyIndex); - if (!modelIndex.isValid() || rowCount(modelIndex) > 0) - continue; - setData(modelIndex, int(action), Qt::EditRole); - } - }); } QString Model::parse(const QByteArray &data) @@ -511,7 +170,7 @@ QString Model::parse(const QByteArray &data) root_ = parse(json); if (root_) - updateState(*root_); + updateStates(); endResetModel(); @@ -593,137 +252,64 @@ std::unique_ptr Model::parse(const QJsonObject &json) const return result; } -void Model::updateProgress(Model::Component &component, const QUrl &url, - int progress) +void Model::updateProgress(const QUrl &url, int progress) { - if (!component.files.empty()) { - for (auto &file : component.files) { - if (!url.isEmpty() && !file.urls.contains(url)) - continue; + if (!root_ || url.isEmpty()) + return; - file.progress = progress; - component.progress = progress; + auto visitor = [this](Component &component, const QUrl &url, int progress, + auto v) -> bool { + if (!component.files.empty()) { + for (auto &file : component.files) { + if (!file.urls.contains(url)) + continue; - for (const auto &file : component.files) - component.progress = std::max(file.progress, component.progress); + file.progress = progress; + component.progress = progress; - const auto index = toIndex(component, int(Column::Progress)); - emit dataChanged(index, index, {Qt::DisplayRole}); + for (const auto &f : qAsConst(component.files)) + component.progress = std::max(f.progress, component.progress); - if (!url.isEmpty()) - break; + const auto index = toIndex(component, int(Column::Progress)); + emit dataChanged(index, index, {Qt::DisplayRole}); + return true; + } + + } else if (!component.children.empty()) { + for (auto &child : component.children) { + if (v(*child, url, progress, v)) + return true; + } } - return; - } + return false; + }; - if (!component.children.empty()) { - for (auto &child : component.children) - updateProgress(*child, url, progress); - return; - } + visitor(*root_, url, progress, visitor); } -void Model::setExpansions(const std::map &expansions) +void Model::setExpansions(const QHash &expansions) { expansions_ = expansions; updateStates(); } -UserActions Model::userActions() const -{ - if (!root_) - return {}; - - UserActions actions; - - const auto visitor = [&actions](const Component &component, auto v) -> void { - if (!component.files.empty()) { - if (component.action == Action::NoAction) - return; - - for (auto &file : component.files) - actions.emplace(component.action, file); - return; - } - - if (!component.children.empty()) { - for (auto &child : component.children) v(*child, v); - return; - } - }; - - visitor(*root_, visitor); - - return actions; -} - void Model::updateStates() { if (!root_) return; - updateState(*root_); + auto visitor = [this](Component &component, auto v) -> void { + if (!component.files.empty()) { + component.state = State::Actual; + for (auto &file : component.files) { + file.expandedPath = expanded(file.rawPath); + const auto fileState = currentState(file); + component.state = std::min(component.state, fileState); + } + auto index = toIndex(component, int(Column::State)); + emit dataChanged(index, index, {Qt::DisplayRole}); - const auto visitor = [this](const QModelIndex &parent, auto v) -> void { - const auto count = rowCount(parent); - if (count == 0) - return; - - emit dataChanged(index(0, int(Column::State), parent), - index(count - 1, int(Column::Action), parent), - {Qt::DisplayRole, Qt::EditRole}); - - for (auto i = 0; i < count; ++i) v(index(0, 0, parent), v); - }; - - visitor(QModelIndex(), visitor); -} - -bool Model::hasUpdates() const -{ - if (!root_) - return false; - - const auto visitor = [](const Component &component, auto v) -> bool { - for (const auto &i : component.children) { - if (i->state == State::UpdateAvailable || v(*i, v)) - return true; - } - return false; - }; - - return visitor(*root_, visitor); -} - -void Model::updateProgress(const QUrl &url, int progress) -{ - if (!root_) - return; - - updateProgress(*root_, url, progress); -} - -void Model::resetProgress() -{ - if (!root_) - return; - - updateProgress(*root_, {}, 0); -} - -void Model::selectAllUpdates() -{ - if (!root_) - return; - - const auto visitor = [this](Component &component, auto v) -> void { - if (component.state == State::UpdateAvailable) { - component.action = Action::Install; - const auto index = toIndex(component, int(Column::Action)); - emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole}); - } - - if (!component.children.empty()) { + } else if (!component.children.empty()) { for (auto &child : component.children) v(*child, v); } }; @@ -731,48 +317,6 @@ void Model::selectAllUpdates() visitor(*root_, visitor); } -void Model::resetActions() -{ - if (!root_) - return; - - const auto visitor = [this](Component &component, auto v) -> void { - if (component.action != Action::NoAction) { - component.action = Action::NoAction; - const auto index = toIndex(component, int(Column::Action)); - emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole}); - } - - if (!component.children.empty()) { - for (auto &child : component.children) v(*child, v); - } - }; - - visitor(*root_, visitor); -} - -void Model::updateState(Model::Component &component) -{ - if (!component.files.empty()) { - std::vector states; - states.reserve(component.files.size()); - - for (auto &file : component.files) { - file.expandedPath = expanded(file.rawPath); - states.push_back(currentState(file)); - } - - component.state = *std::min_element(states.cbegin(), states.cend()); - component.action = Action::NoAction; - return; - } - - if (!component.children.empty()) { - for (auto &child : component.children) updateState(*child); - return; - } -} - State Model::currentState(const File &file) const { if (file.expandedPath.isEmpty() || @@ -806,15 +350,76 @@ QString Model::expanded(const QString &source) const { auto result = source; - for (const auto &expansion : expansions_) { - if (!result.contains(expansion.first)) + for (auto it = expansions_.cbegin(), end = expansions_.cend(); it != end; + ++it) { + if (!result.contains(it.key())) continue; - result.replace(expansion.first, expansion.second); + result.replace(it.key(), it.value()); } return result; } +bool Model::hasUpdates() const +{ + if (!root_) + return false; + + const auto visitor = [](const Component &component, auto v) -> bool { + for (const auto &i : component.children) { + if (i->state == State::UpdateAvailable || v(*i, v)) + return true; + } + return false; + }; + + return visitor(*root_, visitor); +} + +void Model::selectAllUpdates() +{ + if (!root_) + return; + + const auto visitor = [this](Component &component, auto v) -> void { + if (component.checkOnly) + return; + + if (component.state == State::UpdateAvailable) + updater_.applyAction(Action::Install, component.files); + + if (!component.children.empty()) { + for (auto &child : component.children) v(*child, v); + } + }; + + visitor(*root_, visitor); +} + +void Model::tryAction(Action action, const QModelIndex &index) +{ + const auto visitor = [this, action](Component &component, auto v) -> void { + if (component.checkOnly) + return; + + const auto &state = component.state; + auto ok = (action == Action::Remove && + (state == State::UpdateAvailable || state == State::Actual)) || + (action == Action::Install && (state == State::UpdateAvailable || + state == State::NotInstalled)); + if (ok) + updater_.applyAction(action, component.files); + + if (!component.children.empty()) { + for (auto &child : component.children) v(*child, v); + } + }; + + auto component = toComponent(index); + SOFT_ASSERT(component, return ); + visitor(*component, visitor); +} + Model::Component *Model::toComponent(const QModelIndex &index) const { return static_cast(index.internalPointer()); @@ -873,10 +478,9 @@ QVariant Model::headerData(int section, Qt::Orientation orientation, return section + 1; const QMap names{ - {Column::Name, tr("Name")}, {Column::State, tr("State")}, - {Column::Action, tr("Action")}, {Column::Size, tr("Size")}, - {Column::Version, tr("Version")}, {Column::Progress, tr("Progress")}, - {Column::Files, tr("Files")}, + {Column::Name, tr("Name")}, {Column::State, tr("State")}, + {Column::Size, tr("Size")}, {Column::Version, tr("Version")}, + {Column::Progress, tr("Progress")}, }; return names.value(Column(section)); } @@ -892,55 +496,15 @@ QVariant Model::data(const QModelIndex &index, int role) const switch (index.column()) { case int(Column::Name): return QObject::tr(qPrintable(ptr->name)); case int(Column::State): return toString(ptr->state); - case int(Column::Action): return toString(ptr->action); case int(Column::Size): return sizeString(ptr->size, 1); case int(Column::Version): return ptr->version; case int(Column::Progress): return ptr->progress > 0 ? ptr->progress : QVariant(); - case int(Column::Files): { - QStringList files; - files.reserve(int(ptr->files.size())); - for (const auto &f : ptr->files) files.append(f.expandedPath); - return files.join(','); - } } return {}; } -bool Model::setData(const QModelIndex &index, const QVariant &value, int role) -{ - if (!index.isValid() || role != Qt::EditRole) - return false; - - auto ptr = toComponent(index); - SOFT_ASSERT(ptr, return false); - - if (index.column() != int(Column::Action)) - return false; - - const auto newAction = Action( - std::clamp(value.toInt(), int(Action::NoAction), int(Action::Install))); - if (ptr->action == newAction) - return false; - - if (newAction != Action::NoAction) { - const QMap> supported{ - {State::NotAvailable, {}}, - {State::Actual, {Action::Remove}}, - {State::NotInstalled, {Action::Install}}, - {State::UpdateAvailable, {Action::Remove, Action::Install}}, - }; - if (!supported[ptr->state].contains(newAction)) - return false; - } - - ptr->action = newAction; - emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole}); - - return true; -} - Qt::ItemFlags Model::flags(const QModelIndex &index) const { auto ptr = toComponent(index); @@ -954,6 +518,79 @@ Qt::ItemFlags Model::flags(const QModelIndex &index) const return result; } +// + +Loader::Loader(Updater &updater) + : updater_(updater) + , network_(new QNetworkAccessManager(this)) +{ + network_->setRedirectPolicy( + QNetworkRequest::RedirectPolicy::NoLessSafeRedirectPolicy); + connect(network_, &QNetworkAccessManager::finished, // + this, &Loader::handleReply); +} + +void Loader::download(const Urls &urls) +{ + start(urls, {}, {}); +} + +void Loader::start(const Urls &urls, const QUrl &previous, const QString &error) +{ + if (!error.isEmpty()) + qCritical() << error; + + if (urls.isEmpty()) { + if (!previous.isEmpty()) + updater_.downloadFailed(previous, error); + return; + } + + auto leftUrls = urls; + const auto current = leftUrls.takeFirst(); + auto reply = network_->get(QNetworkRequest(current)); + downloads_.insert(reply, leftUrls); + + connect(reply, &QNetworkReply::downloadProgress, // + this, [this, current](qint64 bytesSent, qint64 bytesTotal) { + updater_.updateProgress(current, bytesSent, bytesTotal); + }); + + if (reply->error() == QNetworkReply::NoError) { + updater_.updateProgress(current, -1, -1); + return; + } + + handleReply(reply); +} + +void Loader::handleReply(QNetworkReply *reply) +{ + reply->deleteLater(); + + SOFT_ASSERT(downloads_.contains(reply), return ); + const auto leftUrls = downloads_.take(reply); + const auto url = reply->request().url(); + + if (reply->error() != QNetworkReply::NoError) { + const auto error = tr("Failed to download file\n%1. Error %2") + .arg(reply->url().toString(), reply->errorString()); + start(leftUrls, url, error); + return; + } + + const auto replyData = reply->readAll(); + if (replyData.isEmpty()) { + const auto error = tr("Empty data downloaded from\n%1").arg(url.toString()); + start(leftUrls, url, error); + return; + } + + updater_.downloaded(url, replyData); +} + +// + UpdateDelegate::UpdateDelegate(QObject *parent) : QStyledItemDelegate(parent) { @@ -963,91 +600,33 @@ void UpdateDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { - if (index.column() == int(Model::Column::Progress) && - !index.data().isNull()) { - QStyleOptionProgressBar progressBarOption; - progressBarOption.rect = option.rect; - progressBarOption.minimum = 0; - progressBarOption.maximum = 100; - const auto progress = index.data().toInt(); - progressBarOption.progress = progress; - progressBarOption.text = QString::number(progress) + "%"; - progressBarOption.textVisible = true; - - QApplication::style()->drawControl(QStyle::CE_ProgressBar, - &progressBarOption, painter); + if (index.column() != int(Model::Column::Progress) || index.data().isNull()) { + QStyledItemDelegate::paint(painter, option, index); return; } - QStyledItemDelegate::paint(painter, option, index); + QStyleOptionProgressBar progressBarOption; + progressBarOption.rect = option.rect; + progressBarOption.minimum = 0; + progressBarOption.maximum = 100; + const auto progress = index.data().toInt(); + progressBarOption.progress = progress; + progressBarOption.text = QString::number(progress) + "%"; + progressBarOption.textVisible = true; + + QApplication::style()->drawControl(QStyle::CE_ProgressBar, &progressBarOption, + painter); } -Installer::Installer(const UserActions &actions) - : actions_(actions) -{ -} - -bool Installer::commit() -{ - if (!checkIsPossible()) - return false; - - for (const auto &action : actions_) { - const auto &file = action.second; - - if (action.first == Action::Remove) - remove(file); - else if (action.first == Action::Install) - install(file); - } - - return errors_.isEmpty(); -} - -bool Installer::checkIsPossible() -{ - errors_.clear(); - - for (const auto &action : actions_) { - const auto &file = action.second; - - if (action.first == Action::Remove) - checkRemove(file); - else if (action.first == Action::Install) - checkInstall(file); - } - errors_.removeDuplicates(); - - return errors_.isEmpty(); -} - -void Installer::checkRemove(const File &file) -{ - QFileInfo installDir(QFileInfo(file.expandedPath).absolutePath()); - if (!QFile::exists(file.expandedPath)) - return; - - if (installDir.exists() && !installDir.isWritable()) { - errors_.append( - QApplication::translate("Updates", "Directory is not writable\n%1") - .arg(installDir.absolutePath())); - } -} +// void Installer::checkInstall(const File &file) { - if (!QFileInfo::exists(file.downloadPath)) { - errors_.append( - QApplication::translate("Updates", "Downloaded file not exists\n%1") - .arg(file.downloadPath)); - // no return - } - QFileInfo installDir(QFileInfo(file.expandedPath).absolutePath()); if (installDir.exists() && !installDir.isWritable()) { - errors_.append( + error_ += QApplication::translate("Updates", "Directory is not writable\n%1") - .arg(installDir.absolutePath())); + .arg(installDir.absolutePath()); } } @@ -1058,110 +637,328 @@ void Installer::remove(const File &file) return; if (!f.remove()) { - errors_.append(QApplication::translate( - "Updates", "Failed to remove file\n%1\nError %2") - .arg(f.fileName(), f.errorString())); + error_ += QApplication::translate("Updates", + "Failed to remove file\n%1\nError %2") + .arg(f.fileName(), f.errorString()); } } -void Installer::install(const File &file) +void Installer::install(const File &file, const QByteArray &data) { auto installDir = QFileInfo(file.expandedPath).absoluteDir(); if (!installDir.exists() && !installDir.mkpath(".")) { - errors_.append( - QApplication::translate("Updates", "Failed to create path\n%1") - .arg(installDir.absolutePath())); + error_ += QApplication::translate("Updates", "Failed to create path\n%1") + .arg(installDir.absolutePath()); return; } + QTemporaryFile tmp; + if (!tmp.open()) { + error_ += QApplication::translate( + "Updates", "Failed to create temp file\n%1\nError %2") + .arg(tmp.fileName(), tmp.errorString()); + return; + } + + const auto wrote = tmp.write(data); + if (wrote != data.size()) { + error_ += QApplication::translate( + "Updates", "Failed to write to temp file\n%1\nError %2") + .arg(tmp.fileName(), tmp.errorString()); + return; + } + + tmp.close(); + QFile existing(file.expandedPath); if (existing.exists() && !existing.remove()) { - errors_.append(QApplication::translate( - "Updates", "Failed to remove file\n%1\nError %2") - .arg(existing.fileName(), existing.errorString())); + error_ += QApplication::translate("Updates", + "Failed to remove file\n%1\nError %2") + .arg(existing.fileName(), existing.errorString()); return; } - QFile f(file.downloadPath); - if (!f.rename(file.expandedPath)) { - errors_.append(QApplication::translate( - "Updates", "Failed to move file\n%1\nto %2\nError %3") - .arg(f.fileName(), file.expandedPath, f.errorString())); + if (!tmp.copy(file.expandedPath)) { + error_ += QApplication::translate( + "Updates", "Failed to copy file\n%1\nto %2\nError %3") + .arg(tmp.fileName(), file.expandedPath, tmp.errorString()); return; } } -QString Installer::errorString() const +const QString &Installer::error() const { - return errors_.join('\n'); + return error_; } -AutoChecker::AutoChecker(Loader &loader, QObject *parent) - : QObject(parent) - , loader_(loader) +// + +AutoChecker::AutoChecker(Updater &updater, int intervalDays, + const QDateTime &lastCheck) + : updater_(updater) + , checkIntervalDays_(intervalDays) + , lastCheckDate_(lastCheck) { - SOFT_ASSERT(loader.model(), return ); - connect(loader.model(), &Model::modelReset, // - this, &AutoChecker::handleModelReset); + connect(&updater_, &Updater::checkedForUpdates, // + this, &AutoChecker::updateLastCheckDate); + scheduleNextCheck(); } AutoChecker::~AutoChecker() = default; -bool AutoChecker::isLastCheckDateChanged() const -{ - return isLastCheckDateChanged_; -} - -QDateTime AutoChecker::lastCheckDate() const +const QDateTime &AutoChecker::lastCheckDate() const { return lastCheckDate_; } -void AutoChecker::setCheckIntervalDays(int days) -{ - checkIntervalDays_ = days; - scheduleNextCheck(); -} - -void AutoChecker::setLastCheckDate(const QDateTime &dt) -{ - isLastCheckDateChanged_ = false; - - lastCheckDate_ = dt; - if (!lastCheckDate_.isValid()) - lastCheckDate_ = QDateTime::currentDateTime(); - - scheduleNextCheck(); -} - void AutoChecker::scheduleNextCheck() { if (timer_) timer_->stop(); - if (checkIntervalDays_ < 1 || !lastCheckDate_.isValid()) + if (checkIntervalDays_ < 1) return; if (!timer_) { timer_ = std::make_unique(); timer_->setSingleShot(true); connect(timer_.get(), &QTimer::timeout, // - &loader_, &Loader::checkForUpdates); + &updater_, &Updater::checkForUpdates); } - auto nextTime = lastCheckDate_.addDays(checkIntervalDays_); const auto now = QDateTime::currentDateTime(); - if (nextTime < now) + const auto &last = lastCheckDate_.isValid() ? lastCheckDate_ : now; + auto nextTime = last.addDays(checkIntervalDays_); + if (nextTime <= now) nextTime = now.addSecs(5); timer_->start(now.msecsTo(nextTime)); } -void AutoChecker::handleModelReset() +void AutoChecker::updateLastCheckDate() { lastCheckDate_ = QDateTime::currentDateTime(); - isLastCheckDateChanged_ = true; scheduleNextCheck(); } +// + +Updater::Updater(const QVector &updateUrls) + : model_(std::make_unique(*this)) + , loader_(std::make_unique(*this)) + , updateUrls_(updateUrls) +{ + std::random_device device; + std::mt19937 generator(device()); + std::shuffle(updateUrls_.begin(), updateUrls_.end(), generator); +} + +void Updater::initView(QTreeView *view) +{ + view->setSelectionMode(QAbstractItemView::ExtendedSelection); + + auto proxy = new QSortFilterProxyModel(view); + proxy->setSourceModel(model_.get()); + + view->setModel(proxy); + view->setItemDelegate(new update::UpdateDelegate(view)); + view->setSortingEnabled(true); + view->sortByColumn(int(Model::Column::Name), Qt::AscendingOrder); + view->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(view, &QAbstractItemView::doubleClicked, // + this, &Updater::handleModelDoubleClick); + connect(view, &QAbstractItemView::customContextMenuRequested, // + this, &Updater::showModelContextMenu); +} + +void Updater::setExpansions(const QHash &expansions) +{ + model_->setExpansions(expansions); +} + +void Updater::checkForUpdates() +{ + loader_->download(updateUrls_); +} + +void Updater::applyAction(Action action, const QVector &files) +{ + for (const auto &file : files) { + LTRACE() << "applyAction" << int(action) << file.rawPath; + + if (action == Action::Remove) { + Installer installer; + installer.remove(file); + if (!installer.error().isEmpty()) { + emit error(installer.error()); + continue; + } + model_->updateStates(); + emit updated(); + continue; + } + + if (action == Action::Install) { + if (file.urls.isEmpty() || findDownload(file.urls.first()) != -1) + continue; + + Installer installer; + installer.checkInstall(file); + + if (!installer.error().isEmpty()) { + emit error(installer.error()); + continue; + } + + downloading_.push_back(file); + loader_->download(file.urls); + continue; + } + } +} + +void Updater::downloaded(const QUrl &url, const QByteArray &data) +{ + LTRACE() << "downloaded" << url << LARG(data.size()); + + if (updateUrls_.contains(url)) { + const auto errors = model_->parse(data); + emit checkedForUpdates(); + if (!errors.isEmpty()) { + emit error(errors); + return; + } + if (model_->hasUpdates()) + emit updatesAvailable(); + return; + } + + model_->updateProgress(url, 0); + + const auto index = findDownload(url); + if (index == -1) + return; + + const auto file = downloading_.takeAt(index); + LTRACE() << "downloaded file" << file.expandedPath; + + const auto mustUnpack = + url.toString().endsWith(".zip") && !file.expandedPath.endsWith(".zip"); + const auto unpacked = mustUnpack ? unpack(data) : data; + if (unpacked.isEmpty()) { + emit error(tr("Empty data unpacked from\n%1").arg(url.toString())); + return; + } + + Installer installer; + installer.install(file, data); + if (!installer.error().isEmpty()) { + emit error(installer.error()); + return; + } + + model_->updateStates(); + emit updated(); +} + +void Updater::updateProgress(const QUrl &url, qint64 bytesSent, + qint64 bytesTotal) +{ + auto progress = bytesTotal < 1 ? 1 : int(100.0 * bytesSent / bytesTotal); + model_->updateProgress(url, progress); +} + +void Updater::downloadFailed(const QUrl &url, const QString &error) +{ + if (updateUrls_.contains(url)) { + emit checkedForUpdates(); + emit this->error(error); + return; + } + + model_->updateProgress(url, 0); + + const auto index = findDownload(url); + if (index == -1) + return; + + downloading_.removeAt(index); + emit this->error(error); +} + +QDateTime Updater::lastUpdateCheck() const +{ + if (!autoChecker_) + return {}; + return autoChecker_->lastCheckDate(); +} + +void Updater::setAutoUpdate(int intervalDays, const QDateTime &lastCheck) +{ + if (intervalDays < 1) { + autoChecker_.reset(); + return; + } + + autoChecker_ = std::make_unique(*this, intervalDays, lastCheck); +} + +void Updater::handleModelDoubleClick(const QModelIndex &index) +{ + if (!index.isValid()) + return; + + model_->tryAction(Action::Install, fromProxy(index)); +} + +void Updater::showModelContextMenu() +{ + QMenu menu; + auto install = menu.addAction(toString(Action::Install)); + menu.addAction(toString(Action::Remove)); + menu.addSeparator(); + auto updateAll = menu.addAction(tr("Update all")); + + const auto choice = menu.exec(QCursor::pos()); + if (!choice) + return; + + if (choice == updateAll) { + model_->selectAllUpdates(); + return; + } + + auto view = qobject_cast(sender()); + SOFT_ASSERT(view, return ); + + const auto selection = view->selectionModel(); + SOFT_ASSERT(selection, return ); + const auto indexes = selection->selectedRows(int(Model::Column::Name)); + if (indexes.isEmpty()) + return; + + const auto action = choice == install ? Action::Install : Action::Remove; + for (const auto &index : indexes) model_->tryAction(action, fromProxy(index)); +} + +int Updater::findDownload(const QUrl &url) const +{ + auto it = std::find_if(downloading_.cbegin(), downloading_.cend(), + [url](const File &f) { return f.urls.contains(url); }); + if (it == downloading_.end()) + return -1; + return std::distance(downloading_.cbegin(), it); +} + +QModelIndex Updater::fromProxy(const QModelIndex &index) const +{ + if (!index.isValid() || index.model() == model_.get()) + return index; + auto proxy = qobject_cast(index.model()); + if (!proxy) + return {}; + return proxy->mapToSource(index); +} + } // namespace update diff --git a/src/service/updates.h b/src/service/updates.h index a2a8085..0a6b7be 100644 --- a/src/service/updates.h +++ b/src/service/updates.h @@ -11,20 +11,19 @@ class QTreeView; namespace update { enum class State { NotAvailable, NotInstalled, UpdateAvailable, Actual }; -enum class Action { NoAction, Remove, Install }; +enum class Action { Install, Remove }; + +class Updater; struct File { QVector urls; QString rawPath; QString expandedPath; - QString downloadPath; QString md5; QDateTime versionDate; int progress{0}; }; -using UserActions = std::multimap; - class UpdateDelegate : public QStyledItemDelegate { Q_OBJECT @@ -38,30 +37,17 @@ class Model : public QAbstractItemModel { Q_OBJECT public: - enum class Column { - Name, - State, - Action, - Size, - Version, - Progress, - Files, - Count - }; + enum class Column { Name, State, Size, Version, Progress, Count }; - explicit Model(QObject* parent = nullptr); - - void initView(QTreeView* view); + explicit Model(Updater& updater); QString parse(const QByteArray& data); - void setExpansions(const std::map& expansions); - UserActions userActions() const; + void setExpansions(const QHash& expansions); void updateStates(); bool hasUpdates() const; void updateProgress(const QUrl& url, int progress); - void resetProgress(); void selectAllUpdates(); - void resetActions(); + void tryAction(Action action, const QModelIndex& index); QModelIndex index(int row, int column, const QModelIndex& parent) const override; @@ -72,16 +58,13 @@ public: int role) const override; QVariant data(const QModelIndex& index, int role) const override; Qt::ItemFlags flags(const QModelIndex& index) const override; - bool setData(const QModelIndex& index, const QVariant& value, - int role) override; private: struct Component { QString name; State state{State::NotAvailable}; - Action action{Action::NoAction}; QString version; - std::vector files; + QVector files; bool checkOnly{false}; std::vector> children; Component* parent{nullptr}; @@ -91,33 +74,14 @@ private: }; std::unique_ptr parse(const QJsonObject& json) const; - void updateProgress(Component& component, const QUrl& url, int progress); - void updateState(Component& component); State currentState(const File& file) const; QString expanded(const QString& source) const; Component* toComponent(const QModelIndex& index) const; QModelIndex toIndex(const Component& component, int column) const; + Updater& updater_; std::unique_ptr root_; - std::map expansions_; -}; - -class Installer -{ -public: - explicit Installer(const UserActions& actions); - bool commit(); - QString errorString() const; - -private: - void remove(const File& file); - void install(const File& file); - void checkRemove(const File& file); - void checkInstall(const File& file); - bool checkIsPossible(); - - UserActions actions_; - QStringList errors_; + QHash expansions_; }; class Loader : public QObject @@ -125,60 +89,85 @@ class Loader : public QObject Q_OBJECT public: using Urls = QVector; - explicit Loader(const Urls& updateUrls, QObject* parent = nullptr); + explicit Loader(Updater& updater); - void checkForUpdates(); - void applyUserActions(); - Model* model() const; - -signals: - void updatesAvailable(); - void updated(); - void error(const QString& error); + void download(const Urls& urls); private: - void addError(const QString& text); - void dumpErrors(); + void start(const Urls& urls, const QUrl& previous, const QString& error); void handleReply(QNetworkReply* reply); - bool handleComponentReply(QNetworkReply* reply); - void handleUpdateReply(QNetworkReply* reply); - QString toError(QNetworkReply& reply) const; - void finishUpdate(const QString& error = {}); - void commitUpdate(); - void updateProgress(qint64 bytesSent, qint64 bytesTotal); - bool startDownload(File& file); - void startDownloadUpdates(const QUrl& previous); + Updater& updater_; QNetworkAccessManager* network_; - Model* model_; - Urls updateUrls_; - QString downloadPath_; - std::map downloads_; - QStringList errors_; - UserActions currentActions_; + QHash downloads_; +}; + +class Installer +{ +public: + void remove(const File& file); + void install(const File& file, const QByteArray& data); + void checkInstall(const File& file); + const QString& error() const; + +private: + QString error_; }; class AutoChecker : public QObject { Q_OBJECT public: - explicit AutoChecker(Loader& loader, QObject* parent = nullptr); + AutoChecker(Updater& updater, int intervalDays, const QDateTime& lastCheck); ~AutoChecker(); - bool isLastCheckDateChanged() const; - QDateTime lastCheckDate() const; - void setCheckIntervalDays(int days); - void setLastCheckDate(const QDateTime& dt); + const QDateTime& lastCheckDate() const; private: - void handleModelReset(); + void updateLastCheckDate(); void scheduleNextCheck(); - Loader& loader_; - bool isLastCheckDateChanged_{false}; + Updater& updater_; int checkIntervalDays_{0}; QDateTime lastCheckDate_; std::unique_ptr timer_; }; +class Updater : public QObject +{ + Q_OBJECT +public: + explicit Updater(const QVector& updateUrls); + + void initView(QTreeView* view); + void setExpansions(const QHash& expansions); + void checkForUpdates(); + + QDateTime lastUpdateCheck() const; + void setAutoUpdate(int intervalDays, const QDateTime& lastCheck); + + void applyAction(Action action, const QVector& files); + void downloaded(const QUrl& url, const QByteArray& data); + void updateProgress(const QUrl& url, qint64 bytesSent, qint64 bytesTotal); + void downloadFailed(const QUrl& url, const QString& error); + +signals: + void checkedForUpdates(); + void updatesAvailable(); + void updated(); + void error(const QString& error); + +private: + void handleModelDoubleClick(const QModelIndex& index); + void showModelContextMenu(); + int findDownload(const QUrl& url) const; + QModelIndex fromProxy(const QModelIndex& index) const; + + std::unique_ptr model_; + std::unique_ptr loader_; + std::unique_ptr autoChecker_; + QVector updateUrls_; + QVector downloading_; +}; + } // namespace update diff --git a/src/settingseditor.cpp b/src/settingseditor.cpp index 76f9e4d..7329ede 100644 --- a/src/settingseditor.cpp +++ b/src/settingseditor.cpp @@ -9,7 +9,7 @@ #include -SettingsEditor::SettingsEditor(Manager &manager, update::Loader &updater) +SettingsEditor::SettingsEditor(Manager &manager, update::Updater &updater) : ui(new Ui::SettingsEditor) , manager_(manager) , updater_(updater) @@ -92,16 +92,15 @@ SettingsEditor::SettingsEditor(Manager &manager, update::Loader &updater) this, [this] { pickColor(ColorContext::Bagkround); }); // updates - updater.model()->initView(ui->updatesView); + ui->updatesView->header()->setObjectName("updatesHeader"); + updater_.initView(ui->updatesView); adjustUpdatesView(); - connect(updater_.model(), &QAbstractItemModel::modelReset, // + connect(&updater_, &update::Updater::checkedForUpdates, // this, &SettingsEditor::adjustUpdatesView); - connect(&updater_, &update::Loader::updated, // + connect(&updater_, &update::Updater::updated, // this, &SettingsEditor::adjustUpdatesView); connect(ui->checkUpdates, &QPushButton::clicked, // - &updater_, &update::Loader::checkForUpdates); - connect(ui->applyUpdates, &QPushButton::clicked, // - &updater_, &update::Loader::applyUserActions); + &updater_, &update::Updater::checkForUpdates); // about { @@ -317,8 +316,6 @@ void SettingsEditor::updateTranslators() void SettingsEditor::adjustUpdatesView() { - ui->updatesView->resizeColumnToContents(int(update::Model::Column::Name)); - if (ui->tessdataPath->text().isEmpty()) // not inited yet return; diff --git a/src/settingseditor.h b/src/settingseditor.h index 0d41399..168eaae 100644 --- a/src/settingseditor.h +++ b/src/settingseditor.h @@ -16,7 +16,7 @@ class SettingsEditor : public QDialog Q_OBJECT public: - SettingsEditor(Manager &manager, update::Loader &updater); + SettingsEditor(Manager &manager, update::Updater &updater); ~SettingsEditor(); Settings settings() const; @@ -35,7 +35,7 @@ private: Ui::SettingsEditor *ui; Manager &manager_; - update::Loader &updater_; + update::Updater &updater_; CommonModels models_; QStringList enabledTranslators_; bool wasPortable_{false}; diff --git a/src/settingseditor.ui b/src/settingseditor.ui index 89d4573..5b2bf06 100644 --- a/src/settingseditor.ui +++ b/src/settingseditor.ui @@ -613,33 +613,14 @@ - - - - Qt::Horizontal + + + + true - - - 204 - 20 - - - + - - - - Qt::Horizontal - - - - 203 - 20 - - - - - + @@ -675,20 +656,6 @@ - - - - true - - - - - - - Apply updates - - - diff --git a/src/stfwd.h b/src/stfwd.h index 14ffc5d..f257727 100644 --- a/src/stfwd.h +++ b/src/stfwd.h @@ -22,8 +22,7 @@ class CommonModels; namespace update { -class Loader; -class AutoChecker; +class Updater; } // namespace update using TaskPtr = std::shared_ptr; diff --git a/tests/updates_test.cpp b/tests/updates_test.cpp index 1df5eb2..9296eee 100644 --- a/tests/updates_test.cpp +++ b/tests/updates_test.cpp @@ -14,10 +14,9 @@ const auto f1 = "from1.txt"; const auto t1 = "test/to1.txt"; const auto data = "sample"; -File toFile(const QString& from, const QString& to) +File toFile(const QString& to) { File result; - result.downloadPath = from; result.expandedPath = to; return result; } @@ -47,41 +46,24 @@ bool removeFile(const QString& name) } } // namespace -TEST(UpdateInstaller, Empty) -{ - UserActions actions; - Installer testee(actions); - ASSERT_TRUE(testee.commit()); -} - TEST(UpdateInstaller, SuccessInstall) { ASSERT_TRUE(removeFile(t1)); - ASSERT_TRUE(writeFile(f1, data)); - UserActions actions{{Action::Install, toFile(f1, t1)}}; - Installer testee(actions); - ASSERT_TRUE(testee.commit()); + Installer testee; + testee.install(toFile(t1), data); ASSERT_EQ(data, readFile(t1)); + ASSERT_TRUE(testee.error().isEmpty()); } TEST(UpdateInstaller, SuccessRemove) { ASSERT_TRUE(writeFile(f1, data)); - UserActions actions{{Action::Remove, toFile(f1, f1)}}; - Installer testee(actions); - ASSERT_TRUE(testee.commit()); + Installer testee; + testee.remove(toFile(f1)); ASSERT_FALSE(QFile::exists(f1)); -} - -TEST(UpdateInstaller, FailInstallNoSource) -{ - ASSERT_TRUE(removeFile(f1)); - - UserActions actions{{Action::Install, toFile(f1, t1)}}; - Installer testee(actions); - ASSERT_FALSE(testee.commit()); + ASSERT_TRUE(testee.error().isEmpty()); } TEST(UpdateInstaller, FailInstallNoWritable) @@ -90,9 +72,9 @@ TEST(UpdateInstaller, FailInstallNoWritable) QFile f(t1); ASSERT_FALSE(f.isWritable()); - UserActions actions{{Action::Install, toFile(f1, t1)}}; - Installer testee(actions); - ASSERT_FALSE(testee.commit()); + Installer testee; + testee.install(toFile(t1), data); + ASSERT_FALSE(testee.error().isEmpty()); } TEST(UpdateInstaller, FailRemove) @@ -103,9 +85,9 @@ TEST(UpdateInstaller, FailRemove) return; ASSERT_FALSE(QFile::copy(f1, f1 + "1")); // non writable - UserActions actions{{Action::Remove, toFile(f1, f1)}}; - Installer testee(actions); - ASSERT_FALSE(testee.commit()); + Installer testee; + testee.remove(toFile(f1)); + ASSERT_FALSE(testee.error().isEmpty()); } TEST(UpdateModel, ParseFail) @@ -113,7 +95,8 @@ TEST(UpdateModel, ParseFail) const auto updates = R"({ })"; - Model testee; + Updater updater({}); + Model testee(updater); const auto error = testee.parse(updates); ASSERT_FALSE(error.isEmpty()); @@ -134,7 +117,8 @@ TEST(UpdateModel, Parse) } })"; - Model testee; + Updater updater({}); + Model testee(updater); const auto error = testee.parse(updates); ASSERT_TRUE(error.isEmpty()); @@ -146,40 +130,3 @@ TEST(UpdateModel, Parse) comp1.sibling(comp1.row(), int(Model::Column::Name)).data().toString(); ASSERT_EQ("comp1", comp1Name); } - -TEST(UpdateLoader, InstallFile) -{ - const auto uf = "updates.json"; - const auto url = QUrl::fromLocalFile(QFileInfo(f1).absoluteFilePath()); - const auto updates = QString(R"({ -"version":1, -"comp1":{"files":[{"url":"%1", "path":"./%2", "md5":"1"}]} -})") - .arg(url.toString(), t1); - ASSERT_TRUE(writeFile(uf, updates.toUtf8())); - ASSERT_TRUE(writeFile(f1, updates.toUtf8())); - ASSERT_TRUE(removeFile(t1)); - - Loader testee({QUrl::fromLocalFile(QFileInfo(uf).absoluteFilePath())}); - testee.checkForUpdates(); - QCoreApplication::processEvents(); - - auto model = testee.model(); - ASSERT_NE(nullptr, model); - ASSERT_EQ(1, model->rowCount({})); - - const auto comp1 = model->index(0, int(Model::Column::Action), {}); - model->setData(comp1, int(Action::Install), Qt::EditRole); - - QSignalSpy okSpy(&testee, &Loader::updated); - QSignalSpy errorSpy(&testee, &Loader::error); - - testee.applyUserActions(); - - QCoreApplication::processEvents(); - ASSERT_EQ(1, okSpy.count()); - ASSERT_EQ(0, errorSpy.count()); - - ASSERT_TRUE(QFile::exists(f1)); - ASSERT_EQ(updates, readFile(t1)); -}