mirror of
				https://github.com/ciphervance/supercell-wx.git
				synced 2025-10-31 00:00:04 +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/bin/*.debug | ||||||
|           ${{ github.workspace }}/build/lib/*.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) |     - name: Build AppImage (Linux) | ||||||
|       if: matrix.os == 'ubuntu-22.04' |       if: matrix.os == 'ubuntu-22.04' | ||||||
|       env: |       env: | ||||||
|  |  | ||||||
|  | @ -1,6 +1,10 @@ | ||||||
| cmake_minimum_required(VERSION 3.21) | cmake_minimum_required(VERSION 3.21) | ||||||
| set(PROJECT_NAME supercell-wx) | 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_CMP0054 NEW) | ||||||
| set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) | set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| The MIT License (MIT) | 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 | Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
| of this software and associated documentation files (the "Software"), to deal | 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/placefile_triangles.cpp | ||||||
|                 source/scwx/qt/gl/draw/rectangle.cpp) |                 source/scwx/qt/gl/draw/rectangle.cpp) | ||||||
| set(HDR_MANAGER source/scwx/qt/manager/alert_manager.hpp | 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/font_manager.hpp | ||||||
|                 source/scwx/qt/manager/media_manager.hpp |                 source/scwx/qt/manager/media_manager.hpp | ||||||
|                 source/scwx/qt/manager/placefile_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/timeline_manager.hpp | ||||||
|                 source/scwx/qt/manager/update_manager.hpp) |                 source/scwx/qt/manager/update_manager.hpp) | ||||||
| set(SRC_MANAGER source/scwx/qt/manager/alert_manager.cpp | 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/font_manager.cpp | ||||||
|                 source/scwx/qt/manager/media_manager.cpp |                 source/scwx/qt/manager/media_manager.cpp | ||||||
|                 source/scwx/qt/manager/placefile_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/radar_site_model.cpp | ||||||
|               source/scwx/qt/model/tree_item.cpp |               source/scwx/qt/model/tree_item.cpp | ||||||
|               source/scwx/qt/model/tree_model.cpp) |               source/scwx/qt/model/tree_model.cpp) | ||||||
| set(HDR_REQUEST source/scwx/qt/request/nexrad_file_request.hpp) | set(HDR_REQUEST source/scwx/qt/request/download_request.hpp | ||||||
| set(SRC_REQUEST source/scwx/qt/request/nexrad_file_request.cpp) |                 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 | set(HDR_SETTINGS source/scwx/qt/settings/audio_settings.hpp | ||||||
|                  source/scwx/qt/settings/general_settings.hpp |                  source/scwx/qt/settings/general_settings.hpp | ||||||
|                  source/scwx/qt/settings/map_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/animation_dock_widget.hpp | ||||||
|            source/scwx/qt/ui/collapsible_group.hpp |            source/scwx/qt/ui/collapsible_group.hpp | ||||||
|            source/scwx/qt/ui/county_dialog.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/flow_layout.hpp | ||||||
|            source/scwx/qt/ui/imgui_debug_dialog.hpp |            source/scwx/qt/ui/imgui_debug_dialog.hpp | ||||||
|            source/scwx/qt/ui/imgui_debug_widget.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/open_url_dialog.hpp | ||||||
|            source/scwx/qt/ui/placefile_dialog.hpp |            source/scwx/qt/ui/placefile_dialog.hpp | ||||||
|            source/scwx/qt/ui/placefile_settings_widget.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/radar_site_dialog.hpp | ||||||
|            source/scwx/qt/ui/settings_dialog.hpp |            source/scwx/qt/ui/settings_dialog.hpp | ||||||
|            source/scwx/qt/ui/update_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/animation_dock_widget.cpp | ||||||
|            source/scwx/qt/ui/collapsible_group.cpp |            source/scwx/qt/ui/collapsible_group.cpp | ||||||
|            source/scwx/qt/ui/county_dialog.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/flow_layout.cpp | ||||||
|            source/scwx/qt/ui/imgui_debug_dialog.cpp |            source/scwx/qt/ui/imgui_debug_dialog.cpp | ||||||
|            source/scwx/qt/ui/imgui_debug_widget.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/open_url_dialog.cpp | ||||||
|            source/scwx/qt/ui/placefile_dialog.cpp |            source/scwx/qt/ui/placefile_dialog.cpp | ||||||
|            source/scwx/qt/ui/placefile_settings_widget.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/radar_site_dialog.cpp | ||||||
|            source/scwx/qt/ui/settings_dialog.cpp |            source/scwx/qt/ui/settings_dialog.cpp | ||||||
|            source/scwx/qt/ui/update_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/open_url_dialog.ui | ||||||
|            source/scwx/qt/ui/placefile_dialog.ui |            source/scwx/qt/ui/placefile_dialog.ui | ||||||
|            source/scwx/qt/ui/placefile_settings_widget.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/radar_site_dialog.ui | ||||||
|            source/scwx/qt/ui/settings_dialog.ui |            source/scwx/qt/ui/settings_dialog.ui | ||||||
|            source/scwx/qt/ui/update_dialog.ui) |            source/scwx/qt/ui/update_dialog.ui) | ||||||
|  | @ -608,3 +617,25 @@ install(SCRIPT ${deploy_script_qmaplibre_core} | ||||||
| 
 | 
 | ||||||
| install(SCRIPT ${deploy_script_scwx} | install(SCRIPT ${deploy_script_scwx} | ||||||
|         COMPONENT supercell-wx) |         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
 |    // Check for updates
 | ||||||
|    if (generalSettings.update_notifications_enabled().GetValue()) |    if (generalSettings.update_notifications_enabled().GetValue()) | ||||||
|    { |    { | ||||||
|       boost::asio::post( |       boost::asio::post(threadPool_, | ||||||
|          threadPool_, |                         [this]() | ||||||
|          [this]() { updateManager_->CheckForUpdates(main::kVersionString_); }); |                         { | ||||||
|  |                            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 <boost/json.hpp> | ||||||
| #include <cpr/cpr.h> | #include <cpr/cpr.h> | ||||||
| #include <re2/re2.h> | #include <re2/re2.h> | ||||||
|  | #include <QStandardPaths> | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx | ||||||
| { | { | ||||||
|  | @ -230,6 +231,34 @@ UpdateManager::Impl::FindLatestRelease() | ||||||
|    return {latestRelease, latestReleaseVersion}; |    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() | std::shared_ptr<UpdateManager> UpdateManager::Instance() | ||||||
| { | { | ||||||
|    static std::weak_ptr<UpdateManager> updateManagerReference_ {}; |    static std::weak_ptr<UpdateManager> updateManagerReference_ {}; | ||||||
|  |  | ||||||
|  | @ -27,6 +27,8 @@ public: | ||||||
| 
 | 
 | ||||||
|    bool CheckForUpdates(const std::string& currentVersion = {}); |    bool CheckForUpdates(const std::string& currentVersion = {}); | ||||||
| 
 | 
 | ||||||
|  |    static void RemoveTemporaryReleases(); | ||||||
|  | 
 | ||||||
|    static std::shared_ptr<UpdateManager> Instance(); |    static std::shared_ptr<UpdateManager> Instance(); | ||||||
| 
 | 
 | ||||||
| signals: | 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 | 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>, | Release tag_invoke(boost::json::value_to_tag<Release>, | ||||||
|                    const boost::json::value& jv) |                    const boost::json::value& jv) | ||||||
| { | { | ||||||
|    auto jo = jv.as_object(); |    auto& jo = jv.as_object(); | ||||||
| 
 | 
 | ||||||
|    Release release {}; |    Release release {}; | ||||||
| 
 | 
 | ||||||
|  | @ -24,6 +39,9 @@ Release tag_invoke(boost::json::value_to_tag<Release>, | ||||||
|    release.draft_      = jo.at("draft").as_bool(); |    release.draft_      = jo.at("draft").as_bool(); | ||||||
|    release.prerelease_ = jo.at("prerelease").as_bool(); |    release.prerelease_ = jo.at("prerelease").as_bool(); | ||||||
| 
 | 
 | ||||||
|  |    release.assets_ = | ||||||
|  |       boost::json::value_to<std::vector<ReleaseAsset>>(jo.at("assets")); | ||||||
|  | 
 | ||||||
|    // Optional parameters
 |    // Optional parameters
 | ||||||
|    if (jo.contains("body")) |    if (jo.contains("body")) | ||||||
|    { |    { | ||||||
|  |  | ||||||
|  | @ -13,6 +13,18 @@ namespace types | ||||||
| namespace gh | 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 |  * @brief GitHub Release object | ||||||
|  * |  * | ||||||
|  | @ -25,10 +37,14 @@ struct Release | ||||||
|    std::string body_ {}; |    std::string body_ {}; | ||||||
|    bool        draft_ {}; |    bool        draft_ {}; | ||||||
|    bool        prerelease_ {}; |    bool        prerelease_ {}; | ||||||
|  | 
 | ||||||
|  |    std::vector<ReleaseAsset> assets_ {}; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| Release tag_invoke(boost::json::value_to_tag<Release>, | ReleaseAsset tag_invoke(boost::json::value_to_tag<ReleaseAsset>, | ||||||
|                    const boost::json::value& jv); |                         const boost::json::value& jv); | ||||||
|  | Release      tag_invoke(boost::json::value_to_tag<Release>, | ||||||
|  |                         const boost::json::value& jv); | ||||||
| 
 | 
 | ||||||
| } // namespace gh
 | } // namespace gh
 | ||||||
| } // namespace types
 | } // 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 "update_dialog.hpp" | ||||||
| #include "ui_update_dialog.h" | #include "ui_update_dialog.h" | ||||||
| #include <scwx/qt/main/versions.hpp> | #include <scwx/qt/main/versions.hpp> | ||||||
|  | #include <scwx/qt/manager/download_manager.hpp> | ||||||
| #include <scwx/qt/manager/font_manager.hpp> | #include <scwx/qt/manager/font_manager.hpp> | ||||||
|  | #include <scwx/qt/ui/download_dialog.hpp> | ||||||
|  | #include <scwx/util/logger.hpp> | ||||||
| 
 | 
 | ||||||
| #include <QDesktopServices> | #include <QDesktopServices> | ||||||
| #include <QFontDatabase> | #include <QFontDatabase> | ||||||
|  | #include <QProcess> | ||||||
|  | #include <QStandardPaths> | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx | ||||||
| { | { | ||||||
|  | @ -13,19 +18,29 @@ namespace qt | ||||||
| namespace ui | 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: | public: | ||||||
|    explicit UpdateDialogImpl() = default; |    explicit Impl(UpdateDialog* self) : self_ {self} {}; | ||||||
|    ~UpdateDialogImpl()         = default; |    ~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 downloadUrl_ {}; | ||||||
|  |    std::string installUrl_ {}; | ||||||
|  |    std::string installFilename_ {}; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| UpdateDialog::UpdateDialog(QWidget* parent) : | UpdateDialog::UpdateDialog(QWidget* parent) : | ||||||
|     QDialog(parent), |     QDialog(parent), p {std::make_unique<Impl>(this)}, ui(new Ui::UpdateDialog) | ||||||
|     p {std::make_unique<UpdateDialogImpl>()}, |  | ||||||
|     ui(new Ui::UpdateDialog) |  | ||||||
| { | { | ||||||
|    ui->setupUi(this); |    ui->setupUi(this); | ||||||
| 
 | 
 | ||||||
|  | @ -37,6 +52,8 @@ UpdateDialog::UpdateDialog(QWidget* parent) : | ||||||
|    ui->bannerLabel->setFont(titleFont); |    ui->bannerLabel->setFont(titleFont); | ||||||
| 
 | 
 | ||||||
|    ui->releaseNotesText->setOpenExternalLinks(true); |    ui->releaseNotesText->setOpenExternalLinks(true); | ||||||
|  | 
 | ||||||
|  |    ui->installUpdateButton->setVisible(false); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| UpdateDialog::~UpdateDialog() | UpdateDialog::~UpdateDialog() | ||||||
|  | @ -56,6 +73,27 @@ void UpdateDialog::UpdateReleaseInfo(const std::string&        latestVersion, | ||||||
|       QString::fromStdString(latestRelease.body_)); |       QString::fromStdString(latestRelease.body_)); | ||||||
| 
 | 
 | ||||||
|    p->downloadUrl_ = latestRelease.htmlUrl_; |    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() | 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 ui
 | ||||||
| } // namespace qt
 | } // namespace qt
 | ||||||
| } // namespace scwx
 | } // namespace scwx
 | ||||||
|  |  | ||||||
|  | @ -16,11 +16,10 @@ namespace qt | ||||||
| namespace ui | namespace ui | ||||||
| { | { | ||||||
| 
 | 
 | ||||||
| class UpdateDialogImpl; |  | ||||||
| 
 |  | ||||||
| class UpdateDialog : public QDialog | class UpdateDialog : public QDialog | ||||||
| { | { | ||||||
|    Q_OBJECT |    Q_OBJECT | ||||||
|  |    Q_DISABLE_COPY_MOVE(UpdateDialog) | ||||||
| 
 | 
 | ||||||
| public: | public: | ||||||
|    explicit UpdateDialog(QWidget* parent = nullptr); |    explicit UpdateDialog(QWidget* parent = nullptr); | ||||||
|  | @ -31,11 +30,12 @@ public: | ||||||
| 
 | 
 | ||||||
| private slots: | private slots: | ||||||
|    void on_downloadButton_clicked(); |    void on_downloadButton_clicked(); | ||||||
|  |    void on_installUpdateButton_clicked(); | ||||||
| 
 | 
 | ||||||
| private: | private: | ||||||
|    friend UpdateDialogImpl; |    class Impl; | ||||||
|    std::unique_ptr<UpdateDialogImpl> p; |    std::unique_ptr<Impl> p; | ||||||
|    Ui::UpdateDialog*                 ui; |    Ui::UpdateDialog*     ui; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| } // namespace ui
 | } // namespace ui
 | ||||||
|  |  | ||||||
|  | @ -139,6 +139,13 @@ | ||||||
|         </property> |         </property> | ||||||
|        </widget> |        </widget> | ||||||
|       </item> |       </item> | ||||||
|  |       <item> | ||||||
|  |        <widget class="QPushButton" name="installUpdateButton"> | ||||||
|  |         <property name="text"> | ||||||
|  |          <string>Install Update</string> | ||||||
|  |         </property> | ||||||
|  |        </widget> | ||||||
|  |       </item> | ||||||
|       <item> |       <item> | ||||||
|        <spacer name="horizontalSpacer"> |        <spacer name="horizontalSpacer"> | ||||||
|         <property name="orientation"> |         <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 | 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) | TEST(StringsTest, ParseTokensColor) | ||||||
| { | { | ||||||
|    static const std::string line {"Color: red green blue alpha discarded"}; |    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 | 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 |  * @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/algorithm/string/trim.hpp> | ||||||
| #include <boost/lexical_cast.hpp> | #include <boost/lexical_cast.hpp> | ||||||
|  | #include <fmt/format.h> | ||||||
| 
 | 
 | ||||||
| namespace scwx | namespace scwx | ||||||
| { | { | ||||||
| namespace util | 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> ParseTokens(const std::string&       s, | ||||||
|                                      std::vector<std::string> delimiters, |                                      std::vector<std::string> delimiters, | ||||||
|                                      std::size_t              pos) |                                      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.cpp | ||||||
|                  source/scwx/provider/nexrad_data_provider_factory.cpp |                  source/scwx/provider/nexrad_data_provider_factory.cpp | ||||||
|                  source/scwx/provider/warnings_provider.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/environment.hpp | ||||||
|              include/scwx/util/float.hpp |              include/scwx/util/float.hpp | ||||||
|              include/scwx/util/hash.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/threads.hpp | ||||||
|              include/scwx/util/time.hpp |              include/scwx/util/time.hpp | ||||||
|              include/scwx/util/vectorbuf.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/float.cpp | ||||||
|              source/scwx/util/hash.cpp |              source/scwx/util/hash.cpp | ||||||
|              source/scwx/util/logger.cpp |              source/scwx/util/logger.cpp | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Dan Paulat
						Dan Paulat