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));
-}