mirror of
https://github.com/ciphervance/supercell-wx.git
synced 2025-10-30 19:10:06 +00:00
Merge pull request #172 from dpaulat/feature/installer
Add Windows Installer and Updater
This commit is contained in:
commit
ef1101ac4b
30 changed files with 1304 additions and 23 deletions
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
|
|
@ -195,6 +195,20 @@ jobs:
|
|||
${{ github.workspace }}/build/bin/*.debug
|
||||
${{ github.workspace }}/build/lib/*.debug
|
||||
|
||||
- name: Build Installer (Windows)
|
||||
if: matrix.os == 'windows-2022'
|
||||
shell: pwsh
|
||||
run: |
|
||||
cd build
|
||||
cpack
|
||||
|
||||
- name: Upload Installer (Windows)
|
||||
if: matrix.os == 'windows-2022'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: supercell-wx-installer-${{ matrix.artifact_suffix }}
|
||||
path: ${{ github.workspace }}/build/supercell-wx-*.msi*
|
||||
|
||||
- name: Build AppImage (Linux)
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
env:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
cmake_minimum_required(VERSION 3.21)
|
||||
set(PROJECT_NAME supercell-wx)
|
||||
project(${PROJECT_NAME} C CXX)
|
||||
project(${PROJECT_NAME}
|
||||
VERSION 0.4.3
|
||||
DESCRIPTION "Supercell Wx is a free, open source advanced weather radar viewer."
|
||||
HOMEPAGE_URL "https://github.com/dpaulat/supercell-wx"
|
||||
LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_POLICY_DEFAULT_CMP0054 NEW)
|
||||
set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 Dan Paulat
|
||||
Copyright (c) 2021-2024 Dan Paulat
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
BIN
scwx-qt/res/images/scwx-banner.png
Normal file
BIN
scwx-qt/res/images/scwx-banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
BIN
scwx-qt/res/images/scwx-dialog.png
Normal file
BIN
scwx-qt/res/images/scwx-dialog.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
|
|
@ -86,6 +86,7 @@ set(SRC_GL_DRAW source/scwx/qt/gl/draw/draw_item.cpp
|
|||
source/scwx/qt/gl/draw/placefile_triangles.cpp
|
||||
source/scwx/qt/gl/draw/rectangle.cpp)
|
||||
set(HDR_MANAGER source/scwx/qt/manager/alert_manager.hpp
|
||||
source/scwx/qt/manager/download_manager.hpp
|
||||
source/scwx/qt/manager/font_manager.hpp
|
||||
source/scwx/qt/manager/media_manager.hpp
|
||||
source/scwx/qt/manager/placefile_manager.hpp
|
||||
|
|
@ -98,6 +99,7 @@ set(HDR_MANAGER source/scwx/qt/manager/alert_manager.hpp
|
|||
source/scwx/qt/manager/timeline_manager.hpp
|
||||
source/scwx/qt/manager/update_manager.hpp)
|
||||
set(SRC_MANAGER source/scwx/qt/manager/alert_manager.cpp
|
||||
source/scwx/qt/manager/download_manager.cpp
|
||||
source/scwx/qt/manager/font_manager.cpp
|
||||
source/scwx/qt/manager/media_manager.cpp
|
||||
source/scwx/qt/manager/placefile_manager.cpp
|
||||
|
|
@ -154,8 +156,10 @@ set(SRC_MODEL source/scwx/qt/model/alert_model.cpp
|
|||
source/scwx/qt/model/radar_site_model.cpp
|
||||
source/scwx/qt/model/tree_item.cpp
|
||||
source/scwx/qt/model/tree_model.cpp)
|
||||
set(HDR_REQUEST source/scwx/qt/request/nexrad_file_request.hpp)
|
||||
set(SRC_REQUEST source/scwx/qt/request/nexrad_file_request.cpp)
|
||||
set(HDR_REQUEST source/scwx/qt/request/download_request.hpp
|
||||
source/scwx/qt/request/nexrad_file_request.hpp)
|
||||
set(SRC_REQUEST source/scwx/qt/request/download_request.cpp
|
||||
source/scwx/qt/request/nexrad_file_request.cpp)
|
||||
set(HDR_SETTINGS source/scwx/qt/settings/audio_settings.hpp
|
||||
source/scwx/qt/settings/general_settings.hpp
|
||||
source/scwx/qt/settings/map_settings.hpp
|
||||
|
|
@ -217,6 +221,7 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp
|
|||
source/scwx/qt/ui/animation_dock_widget.hpp
|
||||
source/scwx/qt/ui/collapsible_group.hpp
|
||||
source/scwx/qt/ui/county_dialog.hpp
|
||||
source/scwx/qt/ui/download_dialog.hpp
|
||||
source/scwx/qt/ui/flow_layout.hpp
|
||||
source/scwx/qt/ui/imgui_debug_dialog.hpp
|
||||
source/scwx/qt/ui/imgui_debug_widget.hpp
|
||||
|
|
@ -228,6 +233,7 @@ set(HDR_UI source/scwx/qt/ui/about_dialog.hpp
|
|||
source/scwx/qt/ui/open_url_dialog.hpp
|
||||
source/scwx/qt/ui/placefile_dialog.hpp
|
||||
source/scwx/qt/ui/placefile_settings_widget.hpp
|
||||
source/scwx/qt/ui/progress_dialog.hpp
|
||||
source/scwx/qt/ui/radar_site_dialog.hpp
|
||||
source/scwx/qt/ui/settings_dialog.hpp
|
||||
source/scwx/qt/ui/update_dialog.hpp)
|
||||
|
|
@ -237,6 +243,7 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp
|
|||
source/scwx/qt/ui/animation_dock_widget.cpp
|
||||
source/scwx/qt/ui/collapsible_group.cpp
|
||||
source/scwx/qt/ui/county_dialog.cpp
|
||||
source/scwx/qt/ui/download_dialog.cpp
|
||||
source/scwx/qt/ui/flow_layout.cpp
|
||||
source/scwx/qt/ui/imgui_debug_dialog.cpp
|
||||
source/scwx/qt/ui/imgui_debug_widget.cpp
|
||||
|
|
@ -248,6 +255,7 @@ set(SRC_UI source/scwx/qt/ui/about_dialog.cpp
|
|||
source/scwx/qt/ui/open_url_dialog.cpp
|
||||
source/scwx/qt/ui/placefile_dialog.cpp
|
||||
source/scwx/qt/ui/placefile_settings_widget.cpp
|
||||
source/scwx/qt/ui/progress_dialog.cpp
|
||||
source/scwx/qt/ui/radar_site_dialog.cpp
|
||||
source/scwx/qt/ui/settings_dialog.cpp
|
||||
source/scwx/qt/ui/update_dialog.cpp)
|
||||
|
|
@ -262,6 +270,7 @@ set(UI_UI source/scwx/qt/ui/about_dialog.ui
|
|||
source/scwx/qt/ui/open_url_dialog.ui
|
||||
source/scwx/qt/ui/placefile_dialog.ui
|
||||
source/scwx/qt/ui/placefile_settings_widget.ui
|
||||
source/scwx/qt/ui/progress_dialog.ui
|
||||
source/scwx/qt/ui/radar_site_dialog.ui
|
||||
source/scwx/qt/ui/settings_dialog.ui
|
||||
source/scwx/qt/ui/update_dialog.ui)
|
||||
|
|
@ -608,3 +617,25 @@ install(SCRIPT ${deploy_script_qmaplibre_core}
|
|||
|
||||
install(SCRIPT ${deploy_script_scwx}
|
||||
COMPONENT supercell-wx)
|
||||
|
||||
if (MSVC)
|
||||
set(CPACK_PACKAGE_NAME "Supercell Wx")
|
||||
set(CPACK_PACKAGE_VENDOR "Dan Paulat")
|
||||
set(CPACK_PACKAGE_FILE_NAME "supercell-wx-v${SCWX_VERSION}-windows-x64")
|
||||
set(CPACK_PACKAGE_INSTALL_DIRECTORY "Supercell Wx")
|
||||
set(CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/res/icons/scwx-256.ico")
|
||||
set(CPACK_PACKAGE_CHECKSUM SHA256)
|
||||
set(CPACK_RESOURCE_FILE_LICENSE "${SCWX_DIR}/LICENSE.txt")
|
||||
set(CPACK_GENERATOR WIX)
|
||||
set(CPACK_PACKAGE_EXECUTABLES "supercell-wx;Supercell Wx")
|
||||
set(CPACK_WIX_UPGRADE_GUID 36AD0F51-4D4F-4B5D-AB61-94C6B4E4FE1C)
|
||||
set(CPACK_WIX_UI_BANNER "${CMAKE_CURRENT_SOURCE_DIR}/res/images/scwx-banner.png")
|
||||
set(CPACK_WIX_UI_DIALOG "${CMAKE_CURRENT_SOURCE_DIR}/res/images/scwx-dialog.png")
|
||||
set(CPACK_WIX_TEMPLATE "${CMAKE_CURRENT_SOURCE_DIR}/wix.template.in")
|
||||
set(CPACK_WIX_EXTENSIONS WixUIExtension WiXUtilExtension)
|
||||
|
||||
set(CPACK_INSTALL_CMAKE_PROJECTS
|
||||
"${CMAKE_CURRENT_BINARY_DIR};${CMAKE_PROJECT_NAME};supercell-wx;/")
|
||||
|
||||
include(CPack)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -627,9 +627,13 @@ void MainWindowImpl::AsyncSetup()
|
|||
// Check for updates
|
||||
if (generalSettings.update_notifications_enabled().GetValue())
|
||||
{
|
||||
boost::asio::post(
|
||||
threadPool_,
|
||||
[this]() { updateManager_->CheckForUpdates(main::kVersionString_); });
|
||||
boost::asio::post(threadPool_,
|
||||
[this]()
|
||||
{
|
||||
manager::UpdateManager::RemoveTemporaryReleases();
|
||||
updateManager_->CheckForUpdates(
|
||||
main::kVersionString_);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
280
scwx-qt/source/scwx/qt/manager/download_manager.cpp
Normal file
280
scwx-qt/source/scwx/qt/manager/download_manager.cpp
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
#include <scwx/qt/manager/download_manager.hpp>
|
||||
#include <scwx/util/digest.hpp>
|
||||
#include <scwx/util/logger.hpp>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/asio/thread_pool.hpp>
|
||||
#include <cpr/cpr.h>
|
||||
|
||||
namespace scwx
|
||||
{
|
||||
namespace qt
|
||||
{
|
||||
namespace manager
|
||||
{
|
||||
|
||||
static const std::string logPrefix_ = "scwx::qt::manager::download_manager";
|
||||
static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
|
||||
|
||||
class DownloadManager::Impl
|
||||
{
|
||||
public:
|
||||
explicit Impl(DownloadManager* self) : self_ {self} {}
|
||||
|
||||
~Impl() { threadPool_.join(); }
|
||||
|
||||
boost::asio::thread_pool threadPool_ {1u};
|
||||
|
||||
DownloadManager* self_;
|
||||
};
|
||||
|
||||
DownloadManager::DownloadManager() : p(std::make_unique<Impl>(this)) {}
|
||||
DownloadManager::~DownloadManager() = default;
|
||||
|
||||
void DownloadManager::Download(
|
||||
const std::shared_ptr<request::DownloadRequest>& request)
|
||||
{
|
||||
boost::asio::post(
|
||||
p->threadPool_,
|
||||
[=]()
|
||||
{
|
||||
// Prepare destination file
|
||||
const std::filesystem::path& destinationPath =
|
||||
request->destination_path();
|
||||
|
||||
if (!destinationPath.has_parent_path())
|
||||
{
|
||||
logger_->error("Destination has no parent path: \"{}\"");
|
||||
|
||||
Q_EMIT request->RequestComplete(
|
||||
request::DownloadRequest::CompleteReason::IOError);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const std::filesystem::path parentPath = destinationPath.parent_path();
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if (!std::filesystem::exists(parentPath))
|
||||
{
|
||||
if (!std::filesystem::create_directories(parentPath))
|
||||
{
|
||||
logger_->error("Unable to create download directory: \"{}\"",
|
||||
parentPath.string());
|
||||
|
||||
Q_EMIT request->RequestComplete(
|
||||
request::DownloadRequest::CompleteReason::IOError);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove file if it exists
|
||||
if (std::filesystem::exists(destinationPath))
|
||||
{
|
||||
std::error_code error;
|
||||
if (!std::filesystem::remove(destinationPath, error))
|
||||
{
|
||||
logger_->error(
|
||||
"Unable to remove existing destination file ({}): \"{}\"",
|
||||
error.message(),
|
||||
destinationPath.string());
|
||||
|
||||
Q_EMIT request->RequestComplete(
|
||||
request::DownloadRequest::CompleteReason::IOError);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Open file for writing
|
||||
std::ofstream ofs {destinationPath,
|
||||
std::ios_base::out | std::ios_base::binary |
|
||||
std::ios_base::trunc};
|
||||
if (!ofs.is_open() || !ofs.good())
|
||||
{
|
||||
logger_->error(
|
||||
"Unable to open destination file for writing: \"{}\"",
|
||||
destinationPath.string());
|
||||
|
||||
Q_EMIT request->RequestComplete(
|
||||
request::DownloadRequest::CompleteReason::IOError);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
std::chrono::system_clock::time_point lastUpdated {};
|
||||
cpr::cpr_off_t lastDownloadNow {};
|
||||
cpr::cpr_off_t lastDownloadTotal {};
|
||||
|
||||
// Download file
|
||||
cpr::Response response =
|
||||
cpr::Get(cpr::Url {request->url()},
|
||||
cpr::ProgressCallback(
|
||||
[&](cpr::cpr_off_t downloadTotal,
|
||||
cpr::cpr_off_t downloadNow,
|
||||
cpr::cpr_off_t /* uploadTotal */,
|
||||
cpr::cpr_off_t /* uploadNow */,
|
||||
std::intptr_t /* userdata */)
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
std::chrono::system_clock::time_point now =
|
||||
std::chrono::system_clock::now();
|
||||
|
||||
// Only emit an update every 100ms
|
||||
if ((now > lastUpdated + 100ms ||
|
||||
downloadNow == downloadTotal) &&
|
||||
(downloadNow != lastDownloadNow ||
|
||||
downloadTotal != lastDownloadTotal))
|
||||
{
|
||||
logger_->trace("Downloaded: {} / {}",
|
||||
downloadNow,
|
||||
downloadTotal);
|
||||
|
||||
Q_EMIT request->ProgressUpdated(downloadNow,
|
||||
downloadTotal);
|
||||
|
||||
lastUpdated = now;
|
||||
lastDownloadNow = downloadNow;
|
||||
lastDownloadTotal = downloadTotal;
|
||||
}
|
||||
|
||||
return !request->IsCanceled();
|
||||
}),
|
||||
cpr::WriteCallback(
|
||||
[&](std::string data, std::intptr_t /* userdata */)
|
||||
{
|
||||
// Write file
|
||||
ofs << data;
|
||||
return !request->IsCanceled();
|
||||
}));
|
||||
|
||||
bool ofsGood = ofs.good();
|
||||
ofs.close();
|
||||
|
||||
// Handle error response
|
||||
if (response.error.code != cpr::ErrorCode::OK ||
|
||||
request->IsCanceled() || !ofsGood)
|
||||
{
|
||||
request::DownloadRequest::CompleteReason reason =
|
||||
request::DownloadRequest::CompleteReason::IOError;
|
||||
|
||||
if (request->IsCanceled())
|
||||
{
|
||||
logger_->info("Download request cancelled: {}", request->url());
|
||||
|
||||
reason = request::DownloadRequest::CompleteReason::Canceled;
|
||||
}
|
||||
else if (response.error.code != cpr::ErrorCode::OK)
|
||||
{
|
||||
logger_->error("Error downloading file ({}): {}",
|
||||
response.error.message,
|
||||
request->url());
|
||||
|
||||
reason = request::DownloadRequest::CompleteReason::RemoteError;
|
||||
}
|
||||
else if (!ofsGood)
|
||||
{
|
||||
logger_->error("File I/O error: {}", destinationPath.string());
|
||||
|
||||
reason = request::DownloadRequest::CompleteReason::IOError;
|
||||
}
|
||||
|
||||
std::error_code error;
|
||||
if (!std::filesystem::remove(destinationPath, error))
|
||||
{
|
||||
logger_->error("Unable to remove destination file: {}, {}",
|
||||
destinationPath.string(),
|
||||
error.message());
|
||||
}
|
||||
|
||||
Q_EMIT request->RequestComplete(reason);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle response
|
||||
const auto contentMd5 = response.header.find("content-md5");
|
||||
if (contentMd5 != response.header.cend() &&
|
||||
!contentMd5->second.empty())
|
||||
{
|
||||
// Open file for reading
|
||||
std::ifstream is {destinationPath,
|
||||
std::ios_base::in | std::ios_base::binary};
|
||||
if (!is.is_open() || !is.good())
|
||||
{
|
||||
logger_->error("Unable to open destination file for reading: {}",
|
||||
destinationPath.string());
|
||||
|
||||
Q_EMIT request->RequestComplete(
|
||||
request::DownloadRequest::CompleteReason::IOError);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute MD5
|
||||
std::vector<std::uint8_t> digest {};
|
||||
if (!util::ComputeDigest(EVP_md5(), is, digest))
|
||||
{
|
||||
logger_->error("Failed to compute MD5: {}",
|
||||
destinationPath.string());
|
||||
|
||||
Q_EMIT request->RequestComplete(
|
||||
request::DownloadRequest::CompleteReason::IOError);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Compare calculated MD5 with digest in response header
|
||||
QByteArray expectedDigestArray =
|
||||
QByteArray::fromBase64(contentMd5->second.c_str());
|
||||
std::vector<std::uint8_t> expectedDigest(
|
||||
expectedDigestArray.cbegin(), expectedDigestArray.cend());
|
||||
|
||||
if (digest != expectedDigest)
|
||||
{
|
||||
QByteArray calculatedDigest(
|
||||
reinterpret_cast<char*>(digest.data()), digest.size());
|
||||
|
||||
logger_->error("Digest mismatch: {} != {}",
|
||||
calculatedDigest.toBase64().toStdString(),
|
||||
contentMd5->second);
|
||||
|
||||
Q_EMIT request->RequestComplete(
|
||||
request::DownloadRequest::CompleteReason::DigestError);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
logger_->info("Download complete: {}", request->url());
|
||||
Q_EMIT request->RequestComplete(
|
||||
request::DownloadRequest::CompleteReason::OK);
|
||||
});
|
||||
}
|
||||
|
||||
std::shared_ptr<DownloadManager> DownloadManager::Instance()
|
||||
{
|
||||
static std::weak_ptr<DownloadManager> downloadManagerReference_ {};
|
||||
static std::mutex instanceMutex_ {};
|
||||
|
||||
std::unique_lock lock(instanceMutex_);
|
||||
|
||||
std::shared_ptr<DownloadManager> downloadManager =
|
||||
downloadManagerReference_.lock();
|
||||
|
||||
if (downloadManager == nullptr)
|
||||
{
|
||||
downloadManager = std::make_shared<DownloadManager>();
|
||||
downloadManagerReference_ = downloadManager;
|
||||
}
|
||||
|
||||
return downloadManager;
|
||||
}
|
||||
|
||||
} // namespace manager
|
||||
} // namespace qt
|
||||
} // namespace scwx
|
||||
36
scwx-qt/source/scwx/qt/manager/download_manager.hpp
Normal file
36
scwx-qt/source/scwx/qt/manager/download_manager.hpp
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#pragma once
|
||||
|
||||
#include <scwx/qt/request/download_request.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <QObject>
|
||||
|
||||
namespace scwx
|
||||
{
|
||||
namespace qt
|
||||
{
|
||||
namespace manager
|
||||
{
|
||||
|
||||
class DownloadManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DownloadManager();
|
||||
~DownloadManager();
|
||||
|
||||
void Download(const std::shared_ptr<request::DownloadRequest>& request);
|
||||
|
||||
static std::shared_ptr<DownloadManager> Instance();
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> p;
|
||||
};
|
||||
|
||||
} // namespace manager
|
||||
} // namespace qt
|
||||
} // namespace scwx
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
#include <boost/json.hpp>
|
||||
#include <cpr/cpr.h>
|
||||
#include <re2/re2.h>
|
||||
#include <QStandardPaths>
|
||||
|
||||
namespace scwx
|
||||
{
|
||||
|
|
@ -230,6 +231,34 @@ UpdateManager::Impl::FindLatestRelease()
|
|||
return {latestRelease, latestReleaseVersion};
|
||||
}
|
||||
|
||||
void UpdateManager::RemoveTemporaryReleases()
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
const std::string destination {
|
||||
QStandardPaths::writableLocation(QStandardPaths::TempLocation)
|
||||
.toStdString()};
|
||||
const std::filesystem::path destinationPath {destination};
|
||||
std::filesystem::directory_iterator it {destinationPath};
|
||||
|
||||
for (auto& file : it)
|
||||
{
|
||||
if (file.is_regular_file() && file.path().string().ends_with(".msi") &&
|
||||
file.path().stem().string().starts_with("supercell-wx-"))
|
||||
{
|
||||
logger_->info("Removing temporary installer: {}",
|
||||
file.path().string());
|
||||
|
||||
std::error_code error;
|
||||
if (!std::filesystem::remove(file.path(), error))
|
||||
{
|
||||
logger_->warn("Error removing temporary installer: {}",
|
||||
error.message());
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
std::shared_ptr<UpdateManager> UpdateManager::Instance()
|
||||
{
|
||||
static std::weak_ptr<UpdateManager> updateManagerReference_ {};
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ public:
|
|||
|
||||
bool CheckForUpdates(const std::string& currentVersion = {});
|
||||
|
||||
static void RemoveTemporaryReleases();
|
||||
|
||||
static std::shared_ptr<UpdateManager> Instance();
|
||||
|
||||
signals:
|
||||
|
|
|
|||
57
scwx-qt/source/scwx/qt/request/download_request.cpp
Normal file
57
scwx-qt/source/scwx/qt/request/download_request.cpp
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
#include <scwx/qt/request/download_request.hpp>
|
||||
|
||||
namespace scwx
|
||||
{
|
||||
namespace qt
|
||||
{
|
||||
namespace request
|
||||
{
|
||||
|
||||
static const std::string logPrefix_ = "scwx::qt::request::download_request";
|
||||
|
||||
class DownloadRequest::Impl
|
||||
{
|
||||
public:
|
||||
explicit Impl(const std::string& url,
|
||||
const std::filesystem::path& destinationPath) :
|
||||
url_ {url}, destinationPath_ {destinationPath}
|
||||
{
|
||||
}
|
||||
~Impl() = default;
|
||||
|
||||
const std::string url_;
|
||||
const std::filesystem::path destinationPath_;
|
||||
|
||||
bool canceled_ = false;
|
||||
};
|
||||
|
||||
DownloadRequest::DownloadRequest(const std::string& url,
|
||||
const std::filesystem::path& destinationPath) :
|
||||
p(std::make_unique<Impl>(url, destinationPath))
|
||||
{
|
||||
}
|
||||
DownloadRequest::~DownloadRequest() = default;
|
||||
|
||||
const std::string& DownloadRequest::url() const
|
||||
{
|
||||
return p->url_;
|
||||
}
|
||||
|
||||
const std::filesystem::path& DownloadRequest::destination_path() const
|
||||
{
|
||||
return p->destinationPath_;
|
||||
}
|
||||
|
||||
void DownloadRequest::Cancel()
|
||||
{
|
||||
p->canceled_ = true;
|
||||
}
|
||||
|
||||
bool DownloadRequest::IsCanceled() const
|
||||
{
|
||||
return p->canceled_;
|
||||
}
|
||||
|
||||
} // namespace request
|
||||
} // namespace qt
|
||||
} // namespace scwx
|
||||
52
scwx-qt/source/scwx/qt/request/download_request.hpp
Normal file
52
scwx-qt/source/scwx/qt/request/download_request.hpp
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
#include <QObject>
|
||||
|
||||
namespace scwx
|
||||
{
|
||||
namespace qt
|
||||
{
|
||||
namespace request
|
||||
{
|
||||
|
||||
class DownloadRequest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum class CompleteReason
|
||||
{
|
||||
OK,
|
||||
Canceled,
|
||||
IOError,
|
||||
RemoteError,
|
||||
DigestError
|
||||
};
|
||||
|
||||
explicit DownloadRequest(const std::string& url,
|
||||
const std::filesystem::path& destinationPath);
|
||||
~DownloadRequest();
|
||||
|
||||
const std::string& url() const;
|
||||
const std::filesystem::path& destination_path() const;
|
||||
|
||||
void Cancel();
|
||||
|
||||
bool IsCanceled() const;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> p;
|
||||
|
||||
signals:
|
||||
void ProgressUpdated(std::ptrdiff_t downloadedBytes,
|
||||
std::ptrdiff_t totalBytes);
|
||||
void RequestComplete(CompleteReason reason);
|
||||
};
|
||||
|
||||
} // namespace request
|
||||
} // namespace qt
|
||||
} // namespace scwx
|
||||
|
|
@ -11,10 +11,25 @@ namespace types
|
|||
namespace gh
|
||||
{
|
||||
|
||||
ReleaseAsset tag_invoke(boost::json::value_to_tag<ReleaseAsset>,
|
||||
const boost::json::value& jv)
|
||||
{
|
||||
auto& jo = jv.as_object();
|
||||
|
||||
ReleaseAsset asset {};
|
||||
|
||||
// Required parameters
|
||||
asset.name_ = jo.at("name").as_string();
|
||||
asset.contentType_ = jo.at("content_type").as_string();
|
||||
asset.browserDownloadUrl_ = jo.at("browser_download_url").as_string();
|
||||
|
||||
return asset;
|
||||
}
|
||||
|
||||
Release tag_invoke(boost::json::value_to_tag<Release>,
|
||||
const boost::json::value& jv)
|
||||
{
|
||||
auto jo = jv.as_object();
|
||||
auto& jo = jv.as_object();
|
||||
|
||||
Release release {};
|
||||
|
||||
|
|
@ -24,6 +39,9 @@ Release tag_invoke(boost::json::value_to_tag<Release>,
|
|||
release.draft_ = jo.at("draft").as_bool();
|
||||
release.prerelease_ = jo.at("prerelease").as_bool();
|
||||
|
||||
release.assets_ =
|
||||
boost::json::value_to<std::vector<ReleaseAsset>>(jo.at("assets"));
|
||||
|
||||
// Optional parameters
|
||||
if (jo.contains("body"))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,6 +13,18 @@ namespace types
|
|||
namespace gh
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief GitHub Release Asset object
|
||||
*
|
||||
* <https://docs.github.com/en/rest/releases/assets?apiVersion=2022-11-28>
|
||||
*/
|
||||
struct ReleaseAsset
|
||||
{
|
||||
std::string name_ {};
|
||||
std::string contentType_ {};
|
||||
std::string browserDownloadUrl_ {};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief GitHub Release object
|
||||
*
|
||||
|
|
@ -25,10 +37,14 @@ struct Release
|
|||
std::string body_ {};
|
||||
bool draft_ {};
|
||||
bool prerelease_ {};
|
||||
|
||||
std::vector<ReleaseAsset> assets_ {};
|
||||
};
|
||||
|
||||
Release tag_invoke(boost::json::value_to_tag<Release>,
|
||||
const boost::json::value& jv);
|
||||
ReleaseAsset tag_invoke(boost::json::value_to_tag<ReleaseAsset>,
|
||||
const boost::json::value& jv);
|
||||
Release tag_invoke(boost::json::value_to_tag<Release>,
|
||||
const boost::json::value& jv);
|
||||
|
||||
} // namespace gh
|
||||
} // namespace types
|
||||
|
|
|
|||
105
scwx-qt/source/scwx/qt/ui/download_dialog.cpp
Normal file
105
scwx-qt/source/scwx/qt/ui/download_dialog.cpp
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
#include <scwx/qt/ui/download_dialog.hpp>
|
||||
#include <scwx/util/strings.hpp>
|
||||
|
||||
#include <boost/timer/timer.hpp>
|
||||
#include <fmt/chrono.h>
|
||||
#include <fmt/format.h>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QPushButton>
|
||||
|
||||
namespace scwx
|
||||
{
|
||||
namespace qt
|
||||
{
|
||||
namespace ui
|
||||
{
|
||||
|
||||
class DownloadDialog::Impl
|
||||
{
|
||||
public:
|
||||
explicit Impl() {};
|
||||
~Impl() = default;
|
||||
|
||||
boost::timer::cpu_timer timer_ {};
|
||||
};
|
||||
|
||||
DownloadDialog::DownloadDialog(QWidget* parent) :
|
||||
ProgressDialog(parent), p {std::make_unique<Impl>()}
|
||||
{
|
||||
auto buttonBox = button_box();
|
||||
buttonBox->setStandardButtons(QDialogButtonBox::StandardButton::Ok |
|
||||
QDialogButtonBox::StandardButton::Cancel);
|
||||
buttonBox->button(QDialogButtonBox::StandardButton::Ok)
|
||||
->setText("Install Now");
|
||||
|
||||
setWindowTitle(tr("Download File"));
|
||||
SetRange(0, 100);
|
||||
}
|
||||
|
||||
DownloadDialog::~DownloadDialog() {}
|
||||
|
||||
void DownloadDialog::set_filename(const std::string& filename)
|
||||
{
|
||||
QString label = tr("Downloading %1...").arg(filename.c_str());
|
||||
SetTopLabelText(label);
|
||||
}
|
||||
|
||||
void DownloadDialog::StartDownload()
|
||||
{
|
||||
// Hide the OK button until the download is finished
|
||||
button_box()
|
||||
->button(QDialogButtonBox::StandardButton::Ok)
|
||||
->setVisible(false);
|
||||
|
||||
SetValue(0);
|
||||
SetBottomLabelText(tr("Waiting for download to begin..."));
|
||||
p->timer_.start();
|
||||
show();
|
||||
}
|
||||
|
||||
void DownloadDialog::UpdateProgress(std::ptrdiff_t downloadedBytes,
|
||||
std::ptrdiff_t totalBytes)
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
const std::chrono::nanoseconds elapsed {p->timer_.elapsed().wall};
|
||||
|
||||
const double percentComplete =
|
||||
(totalBytes > 0.0) ? static_cast<double>(downloadedBytes) / totalBytes :
|
||||
0.0;
|
||||
const int progressValue = static_cast<int>(percentComplete * 100.0);
|
||||
|
||||
SetValue(progressValue);
|
||||
|
||||
const std::chrono::seconds timeRemaining =
|
||||
(percentComplete > 0.0) ?
|
||||
std::chrono::duration_cast<std::chrono::seconds>(
|
||||
elapsed / percentComplete - elapsed) :
|
||||
0s;
|
||||
const std::chrono::hours hoursRemaining =
|
||||
std::chrono::duration_cast<std::chrono::hours>(timeRemaining);
|
||||
|
||||
const std::string progressText =
|
||||
fmt::format("{} of {} downloaded ({}:{:%M:%S} remaining)",
|
||||
util::BytesToString(downloadedBytes),
|
||||
util::BytesToString(totalBytes),
|
||||
hoursRemaining.count(),
|
||||
timeRemaining);
|
||||
|
||||
SetBottomLabelText(QString::fromStdString(progressText));
|
||||
}
|
||||
|
||||
void DownloadDialog::FinishDownload()
|
||||
{
|
||||
button_box()->button(QDialogButtonBox::StandardButton::Ok)->setVisible(true);
|
||||
}
|
||||
|
||||
void DownloadDialog::CancelDownload()
|
||||
{
|
||||
SetValue(0);
|
||||
SetBottomLabelText(tr("Error occurred while downloading"));
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace qt
|
||||
} // namespace scwx
|
||||
38
scwx-qt/source/scwx/qt/ui/download_dialog.hpp
Normal file
38
scwx-qt/source/scwx/qt/ui/download_dialog.hpp
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#pragma once
|
||||
|
||||
#include <scwx/qt/ui/progress_dialog.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace scwx
|
||||
{
|
||||
namespace qt
|
||||
{
|
||||
namespace ui
|
||||
{
|
||||
class DownloadDialog : public ProgressDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(DownloadDialog)
|
||||
|
||||
public:
|
||||
explicit DownloadDialog(QWidget* parent = nullptr);
|
||||
~DownloadDialog();
|
||||
|
||||
void set_filename(const std::string& filename);
|
||||
|
||||
public slots:
|
||||
void StartDownload();
|
||||
void UpdateProgress(std::ptrdiff_t downloadedBytes,
|
||||
std::ptrdiff_t totalBytes);
|
||||
void FinishDownload();
|
||||
void CancelDownload();
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> p;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace qt
|
||||
} // namespace scwx
|
||||
66
scwx-qt/source/scwx/qt/ui/progress_dialog.cpp
Normal file
66
scwx-qt/source/scwx/qt/ui/progress_dialog.cpp
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
#include "progress_dialog.hpp"
|
||||
#include "ui_progress_dialog.h"
|
||||
|
||||
namespace scwx
|
||||
{
|
||||
namespace qt
|
||||
{
|
||||
namespace ui
|
||||
{
|
||||
|
||||
class ProgressDialog::Impl
|
||||
{
|
||||
public:
|
||||
explicit Impl() = default;
|
||||
~Impl() = default;
|
||||
};
|
||||
|
||||
ProgressDialog::ProgressDialog(QWidget* parent) :
|
||||
QDialog(parent), p {std::make_unique<Impl>()}, ui(new Ui::ProgressDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
}
|
||||
|
||||
ProgressDialog::~ProgressDialog()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
QDialogButtonBox* ProgressDialog::button_box() const
|
||||
{
|
||||
return ui->buttonBox;
|
||||
}
|
||||
|
||||
void ProgressDialog::SetTopLabelText(const QString& text)
|
||||
{
|
||||
ui->topLabel->setText(text);
|
||||
}
|
||||
|
||||
void ProgressDialog::SetBottomLabelText(const QString& text)
|
||||
{
|
||||
ui->bottomLabel->setText(text);
|
||||
}
|
||||
|
||||
void ProgressDialog::SetMinimum(int minimum)
|
||||
{
|
||||
ui->progressBar->setMinimum(minimum);
|
||||
}
|
||||
|
||||
void ProgressDialog::SetMaximum(int maximum)
|
||||
{
|
||||
ui->progressBar->setMaximum(maximum);
|
||||
}
|
||||
|
||||
void ProgressDialog::SetRange(int minimum, int maximum)
|
||||
{
|
||||
ui->progressBar->setRange(minimum, maximum);
|
||||
}
|
||||
|
||||
void ProgressDialog::SetValue(int value)
|
||||
{
|
||||
ui->progressBar->setValue(value);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace qt
|
||||
} // namespace scwx
|
||||
46
scwx-qt/source/scwx/qt/ui/progress_dialog.hpp
Normal file
46
scwx-qt/source/scwx/qt/ui/progress_dialog.hpp
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class QDialogButtonBox;
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class ProgressDialog;
|
||||
}
|
||||
|
||||
namespace scwx
|
||||
{
|
||||
namespace qt
|
||||
{
|
||||
namespace ui
|
||||
{
|
||||
class ProgressDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(ProgressDialog)
|
||||
|
||||
public:
|
||||
explicit ProgressDialog(QWidget* parent = nullptr);
|
||||
~ProgressDialog();
|
||||
|
||||
protected:
|
||||
QDialogButtonBox* button_box() const;
|
||||
|
||||
public slots:
|
||||
void SetTopLabelText(const QString& text);
|
||||
void SetBottomLabelText(const QString& text);
|
||||
void SetMinimum(int minimum);
|
||||
void SetMaximum(int maximum);
|
||||
void SetRange(int minimum, int maximum);
|
||||
void SetValue(int value);
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> p;
|
||||
Ui::ProgressDialog* ui;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace qt
|
||||
} // namespace scwx
|
||||
85
scwx-qt/source/scwx/qt/ui/progress_dialog.ui
Normal file
85
scwx-qt/source/scwx/qt/ui/progress_dialog.ui
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ProgressDialog</class>
|
||||
<widget class="QDialog" name="ProgressDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>394</width>
|
||||
<height>116</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Dialog</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="topLabel">
|
||||
<property name="text">
|
||||
<string>Downloading supercell-wx-v0.4.4-windows-x64.msi...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
<property name="value">
|
||||
<number>24</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="bottomLabel">
|
||||
<property name="text">
|
||||
<string>25.3 MB of 69.1 MB downloaded (00:00:04 remaining)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>ProgressDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>ProgressDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
|
|
@ -1,10 +1,15 @@
|
|||
#include "update_dialog.hpp"
|
||||
#include "ui_update_dialog.h"
|
||||
#include <scwx/qt/main/versions.hpp>
|
||||
#include <scwx/qt/manager/download_manager.hpp>
|
||||
#include <scwx/qt/manager/font_manager.hpp>
|
||||
#include <scwx/qt/ui/download_dialog.hpp>
|
||||
#include <scwx/util/logger.hpp>
|
||||
|
||||
#include <QDesktopServices>
|
||||
#include <QFontDatabase>
|
||||
#include <QProcess>
|
||||
#include <QStandardPaths>
|
||||
|
||||
namespace scwx
|
||||
{
|
||||
|
|
@ -13,19 +18,29 @@ namespace qt
|
|||
namespace ui
|
||||
{
|
||||
|
||||
class UpdateDialogImpl
|
||||
static const std::string logPrefix_ = "scwx::qt::ui::update_dialog";
|
||||
static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
|
||||
|
||||
class UpdateDialog::Impl
|
||||
{
|
||||
public:
|
||||
explicit UpdateDialogImpl() = default;
|
||||
~UpdateDialogImpl() = default;
|
||||
explicit Impl(UpdateDialog* self) : self_ {self} {};
|
||||
~Impl() = default;
|
||||
|
||||
void HandleAsset(const types::gh::ReleaseAsset& asset);
|
||||
|
||||
UpdateDialog* self_;
|
||||
|
||||
std::shared_ptr<manager::DownloadManager> downloadManager_ {
|
||||
manager::DownloadManager::Instance()};
|
||||
|
||||
std::string downloadUrl_ {};
|
||||
std::string installUrl_ {};
|
||||
std::string installFilename_ {};
|
||||
};
|
||||
|
||||
UpdateDialog::UpdateDialog(QWidget* parent) :
|
||||
QDialog(parent),
|
||||
p {std::make_unique<UpdateDialogImpl>()},
|
||||
ui(new Ui::UpdateDialog)
|
||||
QDialog(parent), p {std::make_unique<Impl>(this)}, ui(new Ui::UpdateDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
|
|
@ -37,6 +52,8 @@ UpdateDialog::UpdateDialog(QWidget* parent) :
|
|||
ui->bannerLabel->setFont(titleFont);
|
||||
|
||||
ui->releaseNotesText->setOpenExternalLinks(true);
|
||||
|
||||
ui->installUpdateButton->setVisible(false);
|
||||
}
|
||||
|
||||
UpdateDialog::~UpdateDialog()
|
||||
|
|
@ -56,6 +73,27 @@ void UpdateDialog::UpdateReleaseInfo(const std::string& latestVersion,
|
|||
QString::fromStdString(latestRelease.body_));
|
||||
|
||||
p->downloadUrl_ = latestRelease.htmlUrl_;
|
||||
|
||||
ui->installUpdateButton->setVisible(false);
|
||||
|
||||
for (auto& asset : latestRelease.assets_)
|
||||
{
|
||||
p->HandleAsset(asset);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateDialog::Impl::HandleAsset(const types::gh::ReleaseAsset& asset)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
if (asset.name_.ends_with(".msi"))
|
||||
{
|
||||
self_->ui->installUpdateButton->setVisible(true);
|
||||
installUrl_ = asset.browserDownloadUrl_;
|
||||
installFilename_ = asset.name_;
|
||||
}
|
||||
#else
|
||||
Q_UNUSED(asset)
|
||||
#endif
|
||||
}
|
||||
|
||||
void UpdateDialog::on_downloadButton_clicked()
|
||||
|
|
@ -66,6 +104,86 @@ void UpdateDialog::on_downloadButton_clicked()
|
|||
}
|
||||
}
|
||||
|
||||
void UpdateDialog::on_installUpdateButton_clicked()
|
||||
{
|
||||
if (!p->installUrl_.empty())
|
||||
{
|
||||
ui->installUpdateButton->setEnabled(false);
|
||||
|
||||
std::string destinationPath {
|
||||
QStandardPaths::writableLocation(QStandardPaths::TempLocation)
|
||||
.toStdString()};
|
||||
|
||||
std::shared_ptr<request::DownloadRequest> request =
|
||||
std::make_shared<request::DownloadRequest>(
|
||||
p->installUrl_,
|
||||
std::filesystem::path(destinationPath) / p->installFilename_);
|
||||
|
||||
DownloadDialog* downloadDialog = new DownloadDialog(this);
|
||||
downloadDialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
// Connect request signals
|
||||
connect(request.get(),
|
||||
&request::DownloadRequest::ProgressUpdated,
|
||||
downloadDialog,
|
||||
&DownloadDialog::UpdateProgress);
|
||||
connect(request.get(),
|
||||
&request::DownloadRequest::RequestComplete,
|
||||
downloadDialog,
|
||||
[=](request::DownloadRequest::CompleteReason reason)
|
||||
{
|
||||
switch (reason)
|
||||
{
|
||||
case request::DownloadRequest::CompleteReason::OK:
|
||||
downloadDialog->FinishDownload();
|
||||
break;
|
||||
|
||||
default:
|
||||
downloadDialog->CancelDownload();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Connect dialog signals
|
||||
connect(
|
||||
downloadDialog,
|
||||
&QDialog::accepted,
|
||||
this,
|
||||
[=, this]()
|
||||
{
|
||||
std::filesystem::path installerPackage =
|
||||
request->destination_path();
|
||||
installerPackage.make_preferred();
|
||||
|
||||
logger_->info("Launching application installer: {}",
|
||||
installerPackage.string());
|
||||
|
||||
if (!QProcess::startDetached(
|
||||
"msiexec.exe",
|
||||
{"/i", QString::fromStdString(installerPackage.string())}))
|
||||
{
|
||||
logger_->error("Failed to launch installer");
|
||||
}
|
||||
|
||||
ui->installUpdateButton->setEnabled(true);
|
||||
});
|
||||
connect(downloadDialog,
|
||||
&QDialog::rejected,
|
||||
this,
|
||||
[=, this]()
|
||||
{
|
||||
request->Cancel();
|
||||
|
||||
ui->installUpdateButton->setEnabled(true);
|
||||
});
|
||||
|
||||
downloadDialog->set_filename(p->installFilename_);
|
||||
downloadDialog->StartDownload();
|
||||
|
||||
p->downloadManager_->Download(request);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace qt
|
||||
} // namespace scwx
|
||||
|
|
|
|||
|
|
@ -16,11 +16,10 @@ namespace qt
|
|||
namespace ui
|
||||
{
|
||||
|
||||
class UpdateDialogImpl;
|
||||
|
||||
class UpdateDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(UpdateDialog)
|
||||
|
||||
public:
|
||||
explicit UpdateDialog(QWidget* parent = nullptr);
|
||||
|
|
@ -31,11 +30,12 @@ public:
|
|||
|
||||
private slots:
|
||||
void on_downloadButton_clicked();
|
||||
void on_installUpdateButton_clicked();
|
||||
|
||||
private:
|
||||
friend UpdateDialogImpl;
|
||||
std::unique_ptr<UpdateDialogImpl> p;
|
||||
Ui::UpdateDialog* ui;
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> p;
|
||||
Ui::UpdateDialog* ui;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
|
|
|||
|
|
@ -139,6 +139,13 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="installUpdateButton">
|
||||
<property name="text">
|
||||
<string>Install Update</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
|
|
|
|||
68
scwx-qt/wix.template.in
Normal file
68
scwx-qt/wix.template.in
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?include "cpack_variables.wxi"?>
|
||||
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension"
|
||||
@CPACK_WIX_CUSTOM_XMLNS_EXPANDED@
|
||||
RequiredVersion="3.6.3303.0">
|
||||
|
||||
<Product Id="$(var.CPACK_WIX_PRODUCT_GUID)"
|
||||
Name="$(var.CPACK_PACKAGE_NAME)"
|
||||
Language="1033"
|
||||
Version="$(var.CPACK_PACKAGE_VERSION)"
|
||||
Manufacturer="$(var.CPACK_PACKAGE_VENDOR)"
|
||||
UpgradeCode="$(var.CPACK_WIX_UPGRADE_GUID)">
|
||||
|
||||
<Package InstallerVersion="301" Compressed="yes" InstallScope="perMachine"/>
|
||||
|
||||
<Media Id="1" Cabinet="media1.cab" EmbedCab="yes"/>
|
||||
|
||||
<MajorUpgrade
|
||||
Schedule="afterInstallInitialize"
|
||||
AllowSameVersionUpgrades="yes"
|
||||
DowngradeErrorMessage="A later version of [ProductName] is already installed. Setup will now exit."/>
|
||||
|
||||
<WixVariable Id="WixUILicenseRtf" Value="$(var.CPACK_WIX_LICENSE_RTF)"/>
|
||||
<Property Id="WIXUI_INSTALLDIR" Value="INSTALL_ROOT"/>
|
||||
|
||||
<?ifdef CPACK_WIX_PRODUCT_ICON?>
|
||||
<Property Id="ARPPRODUCTICON">ProductIcon.ico</Property>
|
||||
<Icon Id="ProductIcon.ico" SourceFile="$(var.CPACK_WIX_PRODUCT_ICON)"/>
|
||||
<?endif?>
|
||||
|
||||
<?ifdef CPACK_WIX_UI_BANNER?>
|
||||
<WixVariable Id="WixUIBannerBmp" Value="$(var.CPACK_WIX_UI_BANNER)"/>
|
||||
<?endif?>
|
||||
|
||||
<?ifdef CPACK_WIX_UI_DIALOG?>
|
||||
<WixVariable Id="WixUIDialogBmp" Value="$(var.CPACK_WIX_UI_DIALOG)"/>
|
||||
<?endif?>
|
||||
|
||||
<FeatureRef Id="ProductFeature"/>
|
||||
|
||||
<UIRef Id="$(var.CPACK_WIX_UI_REF)" />
|
||||
<UIRef Id="WixUI_ErrorProgressText" />
|
||||
|
||||
<UI>
|
||||
<Publish Dialog="ExitDialog"
|
||||
Control="Finish"
|
||||
Event="DoAction"
|
||||
Value="LaunchApplication">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>
|
||||
</UI>
|
||||
|
||||
<util:CloseApplication
|
||||
Id="CloseSupercellWx"
|
||||
Target="supercell-wx.exe"
|
||||
RebootPrompt="no"
|
||||
PromptToContinue="yes"
|
||||
Description="Supercell Wx should be closed before continuing the install."/>
|
||||
|
||||
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Launch Supercell Wx" />
|
||||
<Property Id="WixShellExecTarget" Value="[#CM_FP_bin.supercell_wx.exe]" />
|
||||
<CustomAction Id="LaunchApplication" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" />
|
||||
|
||||
<?include "properties.wxi"?>
|
||||
<?include "product_fragment.wxi"?>
|
||||
</Product>
|
||||
</Wix>
|
||||
|
|
@ -7,6 +7,35 @@ namespace scwx
|
|||
namespace util
|
||||
{
|
||||
|
||||
class BytesToStringTest :
|
||||
public testing::TestWithParam<std::pair<std::ptrdiff_t, std::string>>
|
||||
{
|
||||
};
|
||||
|
||||
TEST_P(BytesToStringTest, BytesToString)
|
||||
{
|
||||
auto& [bytes, expected] = GetParam();
|
||||
|
||||
std::string s = BytesToString(bytes);
|
||||
|
||||
EXPECT_EQ(s, expected);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(StringsTest,
|
||||
BytesToStringTest,
|
||||
testing::Values(std::make_pair(123, "123 bytes"),
|
||||
std::make_pair(1000, "0.98 KB"),
|
||||
std::make_pair(1018, "0.99 KB"),
|
||||
std::make_pair(1024, "1.0 KB"),
|
||||
std::make_pair(1127, "1.1 KB"),
|
||||
std::make_pair(1260, "1.23 KB"),
|
||||
std::make_pair(24012, "23.4 KB"),
|
||||
std::make_pair(353974, "346 KB"),
|
||||
std::make_pair(1024000, "0.98 MB"),
|
||||
std::make_pair(1048576000, "0.98 GB"),
|
||||
std::make_pair(1073741824000,
|
||||
"0.98 TB")));
|
||||
|
||||
TEST(StringsTest, ParseTokensColor)
|
||||
{
|
||||
static const std::string line {"Color: red green blue alpha discarded"};
|
||||
|
|
|
|||
18
wxdata/include/scwx/util/digest.hpp
Normal file
18
wxdata/include/scwx/util/digest.hpp
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#pragma once
|
||||
|
||||
#include <istream>
|
||||
#include <vector>
|
||||
|
||||
#include <openssl/evp.h>
|
||||
|
||||
namespace scwx
|
||||
{
|
||||
namespace util
|
||||
{
|
||||
|
||||
bool ComputeDigest(const EVP_MD* mdtype,
|
||||
std::istream& is,
|
||||
std::vector<std::uint8_t>& digest);
|
||||
|
||||
} // namespace util
|
||||
} // namespace scwx
|
||||
|
|
@ -9,6 +9,16 @@ namespace scwx
|
|||
namespace util
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Print the number of bytes using a dynamic suffix and limited number of
|
||||
* decimal points.
|
||||
*
|
||||
* @param [in] bytes Number of bytes
|
||||
*
|
||||
* @return Human readable size string
|
||||
*/
|
||||
std::string BytesToString(std::ptrdiff_t bytes);
|
||||
|
||||
/**
|
||||
* @brief Parse a list of tokens from a string
|
||||
*
|
||||
|
|
|
|||
82
wxdata/source/scwx/util/digest.cpp
Normal file
82
wxdata/source/scwx/util/digest.cpp
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
#include <scwx/util/digest.hpp>
|
||||
#include <scwx/util/logger.hpp>
|
||||
|
||||
namespace scwx
|
||||
{
|
||||
namespace util
|
||||
{
|
||||
|
||||
static const std::string logPrefix_ = "scwx::util::digest";
|
||||
static const auto logger_ = scwx::util::Logger::Create(logPrefix_);
|
||||
|
||||
bool ComputeDigest(const EVP_MD* mdtype,
|
||||
std::istream& is,
|
||||
std::vector<std::uint8_t>& digest)
|
||||
{
|
||||
int mdsize;
|
||||
EVP_MD_CTX* mdctx = nullptr;
|
||||
|
||||
digest.clear();
|
||||
|
||||
if ((mdsize = EVP_MD_get_size(mdtype)) < 1)
|
||||
{
|
||||
logger_->error("Invalid digest");
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((mdctx = EVP_MD_CTX_new()) == nullptr)
|
||||
{
|
||||
logger_->error("Error allocating a digest context");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EVP_DigestInit_ex(mdctx, mdtype, nullptr))
|
||||
{
|
||||
logger_->error("Message digest initialization failed");
|
||||
EVP_MD_CTX_free(mdctx);
|
||||
return false;
|
||||
}
|
||||
|
||||
is.seekg(0, std::ios_base::end);
|
||||
const std::size_t streamSize = is.tellg();
|
||||
is.seekg(0, std::ios_base::beg);
|
||||
|
||||
std::size_t bytesRead = 0;
|
||||
std::size_t chunkSize = 4096;
|
||||
std::string fileData;
|
||||
fileData.resize(chunkSize);
|
||||
|
||||
while (bytesRead < streamSize)
|
||||
{
|
||||
const std::size_t bytesRemaining = streamSize - bytesRead;
|
||||
const std::size_t readSize = std::min(chunkSize, bytesRemaining);
|
||||
|
||||
is.read(fileData.data(), readSize);
|
||||
|
||||
if (!is.good() || !EVP_DigestUpdate(mdctx, fileData.data(), readSize))
|
||||
{
|
||||
logger_->error("Message digest update failed");
|
||||
EVP_MD_CTX_free(mdctx);
|
||||
return false;
|
||||
}
|
||||
|
||||
bytesRead += readSize;
|
||||
}
|
||||
|
||||
digest.resize(mdsize);
|
||||
|
||||
if (!EVP_DigestFinal_ex(mdctx, digest.data(), nullptr))
|
||||
{
|
||||
logger_->error("Message digest finalization failed");
|
||||
EVP_MD_CTX_free(mdctx);
|
||||
digest.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
EVP_MD_CTX_free(mdctx);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
} // namespace scwx
|
||||
|
|
@ -4,12 +4,76 @@
|
|||
|
||||
#include <boost/algorithm/string/trim.hpp>
|
||||
#include <boost/lexical_cast.hpp>
|
||||
#include <fmt/format.h>
|
||||
|
||||
namespace scwx
|
||||
{
|
||||
namespace util
|
||||
{
|
||||
|
||||
std::string BytesToString(std::ptrdiff_t bytes)
|
||||
{
|
||||
auto FormatNumber = [](double number) -> std::string
|
||||
{
|
||||
int precision;
|
||||
|
||||
// Determine precision
|
||||
if (number >= 100.0)
|
||||
{
|
||||
precision = 0;
|
||||
}
|
||||
else if (number >= 10.0)
|
||||
{
|
||||
precision = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
precision = 2;
|
||||
}
|
||||
|
||||
// Format the number
|
||||
std::string formattedNum = fmt::format("{:.{}f}", number, precision);
|
||||
|
||||
// Remove trailing zeroes
|
||||
std::size_t found = formattedNum.find_last_not_of('0');
|
||||
if (found != std::string::npos && formattedNum[found] == '.')
|
||||
{
|
||||
// Keep one trailing zero if it's a decimal point
|
||||
found++;
|
||||
}
|
||||
formattedNum.erase(found + 1, std::string::npos);
|
||||
|
||||
return formattedNum;
|
||||
};
|
||||
|
||||
// Print with appropriate suffix
|
||||
if (bytes < 1000)
|
||||
{
|
||||
return fmt::format("{} bytes", bytes);
|
||||
}
|
||||
|
||||
double kilobytes = bytes / 1024.0;
|
||||
if (kilobytes < 1000.0)
|
||||
{
|
||||
return fmt::format("{} KB", FormatNumber(kilobytes));
|
||||
}
|
||||
|
||||
double megabytes = kilobytes / 1024.0;
|
||||
if (megabytes < 1000.0)
|
||||
{
|
||||
return fmt::format("{} MB", FormatNumber(megabytes));
|
||||
}
|
||||
|
||||
double gigabytes = megabytes / 1024.0;
|
||||
if (gigabytes < 1000.0)
|
||||
{
|
||||
return fmt::format("{} GB", FormatNumber(gigabytes));
|
||||
}
|
||||
|
||||
double terabytes = gigabytes / 1024.0;
|
||||
return fmt::format("{} TB", FormatNumber(terabytes));
|
||||
}
|
||||
|
||||
std::vector<std::string> ParseTokens(const std::string& s,
|
||||
std::vector<std::string> delimiters,
|
||||
std::size_t pos)
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@ set(SRC_PROVIDER source/scwx/provider/aws_level2_data_provider.cpp
|
|||
source/scwx/provider/nexrad_data_provider.cpp
|
||||
source/scwx/provider/nexrad_data_provider_factory.cpp
|
||||
source/scwx/provider/warnings_provider.cpp)
|
||||
set(HDR_UTIL include/scwx/util/enum.hpp
|
||||
set(HDR_UTIL include/scwx/util/digest.hpp
|
||||
include/scwx/util/enum.hpp
|
||||
include/scwx/util/environment.hpp
|
||||
include/scwx/util/float.hpp
|
||||
include/scwx/util/hash.hpp
|
||||
|
|
@ -80,7 +81,8 @@ set(HDR_UTIL include/scwx/util/enum.hpp
|
|||
include/scwx/util/threads.hpp
|
||||
include/scwx/util/time.hpp
|
||||
include/scwx/util/vectorbuf.hpp)
|
||||
set(SRC_UTIL source/scwx/util/environment.cpp
|
||||
set(SRC_UTIL source/scwx/util/digest.cpp
|
||||
source/scwx/util/environment.cpp
|
||||
source/scwx/util/float.cpp
|
||||
source/scwx/util/hash.cpp
|
||||
source/scwx/util/logger.cpp
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue