diff --git a/Common/Cpp/Exceptions.cpp b/Common/Cpp/Exceptions.cpp index 75d6f2e007..299d43e6c6 100644 --- a/Common/Cpp/Exceptions.cpp +++ b/Common/Cpp/Exceptions.cpp @@ -29,6 +29,14 @@ std::string Exception::to_str() const{ } +ParseException::ParseException(std::string message_string) + : m_message(std::move(message_string)) +{ + std::string str = ParseException::name(); + str += ": " + message(); + std::cerr << str << std::endl; +} + FileException::FileException(Logger* logger, const char* location, std::string message_string, std::string file) diff --git a/Common/Cpp/Exceptions.h b/Common/Cpp/Exceptions.h index 5c27db8b3f..0c6bcb3f29 100644 --- a/Common/Cpp/Exceptions.h +++ b/Common/Cpp/Exceptions.h @@ -77,7 +77,7 @@ class OperationCancelledException : public Exception{ class ParseException : public Exception{ public: ParseException() = default; - ParseException(std::string message) : m_message(std::move(message)) {} + ParseException(std::string message); virtual const char* name() const override{ return "ParseException"; } virtual std::string message() const override{ return m_message; } protected: diff --git a/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp b/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp index 0305055e1e..ac74a8a3c1 100644 --- a/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp +++ b/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp @@ -248,6 +248,7 @@ GlobalSettings::GlobalSettings() PA_ADD_OPTION(TEMP_FOLDER); PA_ADD_OPTION(THEME); PA_ADD_OPTION(USE_PADDLE_OCR); + PA_ADD_OPTION(RESOURCE_DOWNLOAD_TABLE); PA_ADD_OPTION(WINDOW_SIZE); PA_ADD_OPTION(LOG_WINDOW_SIZE); PA_ADD_OPTION(LOG_WINDOW_STARTUP); diff --git a/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.h b/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.h index 52a6e30946..938f354707 100644 --- a/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.h +++ b/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.h @@ -17,6 +17,7 @@ #include "Common/Cpp/Options/StringOption.h" #include "CommonFramework/Panels/SettingsPanel.h" #include "CommonFramework/Panels/PanelTools.h" +#include "CommonFramework/ResourceDownload/ResourceDownloadTable.h" //#include //using std::cout; @@ -124,6 +125,7 @@ class GlobalSettings : public BatchOption, private ConfigOption::Listener, priva Pimpl THEME; BooleanCheckBoxOption USE_PADDLE_OCR; + ResourceDownloadTable RESOURCE_DOWNLOAD_TABLE; Pimpl WINDOW_SIZE; Pimpl LOG_WINDOW_SIZE; BooleanCheckBoxOption LOG_WINDOW_STARTUP; diff --git a/SerialPrograms/Source/CommonFramework/Globals.cpp b/SerialPrograms/Source/CommonFramework/Globals.cpp index f916b2f6eb..439d492553 100644 --- a/SerialPrograms/Source/CommonFramework/Globals.cpp +++ b/SerialPrograms/Source/CommonFramework/Globals.cpp @@ -220,6 +220,10 @@ const std::string& RESOURCE_PATH(){ static std::string path = get_resource_path(); return path; } +const std::string& DOWNLOADED_RESOURCE_PATH(){ + static std::string path = RUNTIME_BASE_PATH() + "DownloadedResources/"; + return path; +} const std::string& TRAINING_PATH(){ static std::string path = get_training_path(); return path; diff --git a/SerialPrograms/Source/CommonFramework/Globals.h b/SerialPrograms/Source/CommonFramework/Globals.h index 55ce1a91ce..754b585e14 100644 --- a/SerialPrograms/Source/CommonFramework/Globals.h +++ b/SerialPrograms/Source/CommonFramework/Globals.h @@ -76,6 +76,10 @@ const std::string& USER_FILE_PATH(); // Resource folder path. Resources include JSON files, images, sound files and others required by // various automation programs. const std::string& RESOURCE_PATH(); + +// Folder path that holds Downloaded resources +const std::string& DOWNLOADED_RESOURCE_PATH(); + // Hold ML training data. const std::string& TRAINING_PATH(); diff --git a/SerialPrograms/Source/CommonFramework/Options/LabelCellOption.cpp b/SerialPrograms/Source/CommonFramework/Options/LabelCellOption.cpp index 66e6eb7416..3308b81f0a 100644 --- a/SerialPrograms/Source/CommonFramework/Options/LabelCellOption.cpp +++ b/SerialPrograms/Source/CommonFramework/Options/LabelCellOption.cpp @@ -6,6 +6,7 @@ #include "Common/Cpp/Containers/Pimpl.tpp" #include "Common/Cpp/Json/JsonValue.h" +#include "Common/Cpp/Concurrency/SpinLock.h" #include "LabelCellOption.h" //#include @@ -17,7 +18,7 @@ namespace PokemonAutomation{ struct LabelCellOption::Data{ -// mutable SpinLock m_lock; + mutable SpinLock m_lock; std::string m_text; // ImageRGB32 m_icon_owner; ImageViewRGB32 m_icon; @@ -80,6 +81,7 @@ LabelCellOption::LabelCellOption( // : m_data(CONSTRUCT_TOKEN, std::move(text), std::move(icon)) //{} const std::string& LabelCellOption::text() const{ + ReadSpinLock lg(m_data->m_lock); return m_data->m_text; } const ImageViewRGB32& LabelCellOption::icon() const{ @@ -94,6 +96,18 @@ JsonValue LabelCellOption::to_json() const{ return JsonValue(); } +void LabelCellOption::set_text(std::string x){ + // sanitize(x); + { + WriteSpinLock lg(m_data->m_lock); + if (m_data->m_text == x){ + return; + } + m_data->m_text = std::move(x); + } + report_value_changed(this); +} + diff --git a/SerialPrograms/Source/CommonFramework/Options/LabelCellOption.h b/SerialPrograms/Source/CommonFramework/Options/LabelCellOption.h index f7afe0b27e..300964d6d3 100644 --- a/SerialPrograms/Source/CommonFramework/Options/LabelCellOption.h +++ b/SerialPrograms/Source/CommonFramework/Options/LabelCellOption.h @@ -45,6 +45,8 @@ class LabelCellOption : public ConfigOptionImpl{ const ImageViewRGB32& icon() const; Resolution resolution() const; + void set_text(std::string x); + virtual void load_json(const JsonValue& json) override; virtual JsonValue to_json() const override; diff --git a/SerialPrograms/Source/CommonFramework/Options/QtWidget/LabelCellWidget.cpp b/SerialPrograms/Source/CommonFramework/Options/QtWidget/LabelCellWidget.cpp index 4964284466..d43eca8839 100644 --- a/SerialPrograms/Source/CommonFramework/Options/QtWidget/LabelCellWidget.cpp +++ b/SerialPrograms/Source/CommonFramework/Options/QtWidget/LabelCellWidget.cpp @@ -24,7 +24,7 @@ LabelCellWidget::~LabelCellWidget(){ LabelCellWidget::LabelCellWidget(QWidget& parent, LabelCellOption& value) : QWidget(&parent) , ConfigWidget(value, *this) -// , m_value(value) + , m_value(value) { QHBoxLayout* layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); @@ -49,6 +49,17 @@ LabelCellWidget::LabelCellWidget(QWidget& parent, LabelCellOption& value) layout->addWidget(m_text, 1); // text->setTextInteractionFlags(Qt::TextBrowserInteraction); // m_text->setOpenExternalLinks(true); + + m_value.add_listener(*this); +} + +void LabelCellWidget::update_value(){ + m_text->setText(QString::fromStdString(m_value.text())); +} +void LabelCellWidget::on_config_value_changed(void* object){ + QMetaObject::invokeMethod(m_text, [this]{ + update_value(); + }, Qt::QueuedConnection); } diff --git a/SerialPrograms/Source/CommonFramework/Options/QtWidget/LabelCellWidget.h b/SerialPrograms/Source/CommonFramework/Options/QtWidget/LabelCellWidget.h index 77dda8709b..14442fda62 100644 --- a/SerialPrograms/Source/CommonFramework/Options/QtWidget/LabelCellWidget.h +++ b/SerialPrograms/Source/CommonFramework/Options/QtWidget/LabelCellWidget.h @@ -25,8 +25,11 @@ class LabelCellWidget : public QWidget, public ConfigWidget{ ~LabelCellWidget(); LabelCellWidget(QWidget& parent, LabelCellOption& value); + virtual void update_value() override; + virtual void on_config_value_changed(void* object) override; + private: -// LabelCellOption& m_value; + LabelCellOption& m_value; QLabel* m_icon = nullptr; QLabel* m_text; }; diff --git a/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadHelpers.cpp b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadHelpers.cpp new file mode 100644 index 0000000000..9d08b31e16 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadHelpers.cpp @@ -0,0 +1,141 @@ +/* Resource Download Helpers + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonFramework/Globals.h" +#include "CommonFramework/Logging/Logger.h" +// #include "CommonFramework/Tools/GlobalThreadPools.h" +#include "CommonFramework/Tools/FileDownloader.h" +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "Common/Cpp/Json/JsonArray.h" +#include "Common/Cpp/Json/JsonObject.h" +#include "Common/Cpp/Filesystem.h" +#include "ResourceDownloadHelpers.h" + +// #include +// #include +// #include + +#include +using std::cout; +using std::endl; + +namespace PokemonAutomation{ + + +ResourceType get_resource_type_from_string(std::string type){ + if (type == "ZippedFolder"){ + return ResourceType::ZIP_FILE; + }else{ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "get_resource_type_from_string: Unknown string."); + } + +} + +std::vector deserialize_resource_list_json(const JsonValue& json){ + std::vector resources; + + try{ + const JsonObject& obj = json.to_object_throw(); + const JsonArray& resource_list = obj.get_array_throw("resourceList"); + for (const JsonValue& resource_val : resource_list){ + const JsonObject& resource_obj = resource_val.to_object_throw(); + + std::string resource_name = resource_obj.get_string_throw("resourceName"); + std::optional version_num = (uint16_t)resource_obj.get_integer_throw("version"); + ResourceType resource_type = get_resource_type_from_string(resource_obj.get_string_throw("Type")); + size_t compressed_bytes = (size_t)resource_obj.get_integer_throw("CompressedBytes"); + size_t decompressed_bytes = (size_t)resource_obj.get_integer_throw("DecompressedBytes"); + std::string url = resource_obj.get_string_throw("URL"); + std::string sha_256 = resource_obj.get_string_throw("SHA_256"); + + DownloadedResourceMetadata resource = { + resource_name, + version_num, + resource_type, + compressed_bytes, + decompressed_bytes, + url, + sha_256 + }; + + resources.emplace_back(std::move(resource)); + + } + + }catch (ParseException&){ + std::cerr << "JSON parsing error. Given JSON file doesn't match the expected format." << endl; + // throw ParseException("JSON parsing error. Given JSON file doesn't match the expected format."); + return std::vector(); + } + + return resources; +} + + +const std::vector& local_resource_download_list(){ + // cout << "local_resource_download_list" << endl; + static std::vector local_resources = deserialize_resource_list_json(load_json_file(RESOURCE_PATH() + "ResourceDownloadList.json")); + + return local_resources; +} + + +JsonValue fetch_resource_download_list_json_from_remote(){ + Logger& logger = global_logger_tagged(); + JsonValue json = + FileDownloader::download_json_file( + logger, + "https://raw.githubusercontent.com/jw098/Packages/refs/heads/download/Resources/ResourceDownloadList.json" + ); + + return json; +} + +const JsonValue& remote_resource_download_list_json(){ + static const JsonValue json = fetch_resource_download_list_json_from_remote(); + + return json; +} + +const std::vector& remote_resource_download_list(){ + // cout << "remote_resource_download_list" << endl; + static std::vector remote_resources = deserialize_resource_list_json(remote_resource_download_list_json()); + + return remote_resources; +} + +std::optional get_resource_version_num(Filesystem::Path folder_path){ + try{ + std::string file_name = folder_path.string() + "/version.json"; + const JsonValue& json = load_json_file(file_name); + + const JsonObject& obj = json.to_object_throw(); + uint16_t version_num = (uint16_t)obj.get_integer_throw("version"); + return version_num; + }catch(...){ + std::cerr << "Unable to determine the version number from version.json." << endl; + return std::nullopt; + } + +} + +ResourceVersionStatus get_version_status(uint16_t expected_version_num, std::optional current_version_num){ + if (!current_version_num.has_value()){ + return ResourceVersionStatus::NOT_APPLICABLE; + } + + if (current_version_num < expected_version_num){ + return ResourceVersionStatus::OUTDATED; + }else if (current_version_num == expected_version_num){ + return ResourceVersionStatus::CURRENT; + }else{ // current > expected + return ResourceVersionStatus::FUTURE_VERSION; + } +} + + + +} diff --git a/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadHelpers.h b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadHelpers.h new file mode 100644 index 0000000000..f9775a7c5b --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadHelpers.h @@ -0,0 +1,61 @@ +/* Resource Download Helpers + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_ResourceDownloadHelpers_H +#define PokemonAutomation_ResourceDownloadHelpers_H + +#include +#include + + +namespace PokemonAutomation{ + +namespace Filesystem{ + class Path; +} + + +enum class ResourceType{ + ZIP_FILE, +}; + +struct DownloadedResourceMetadata{ + std::string resource_name; + std::optional version_num; + ResourceType resource_type; + size_t size_compressed_bytes; + size_t size_decompressed_bytes; + std::string url; + std::string sha_256; +}; + +enum class ResourceVersionStatus{ + CURRENT, + OUTDATED, // still used, but newer version available + FUTURE_VERSION, // current version number is greater than the expected version number + NOT_APPLICABLE, // resource not downloaded locally, so can't get its version + // RETIRED, // no longer used + // BLANK, // not yet fetched version info from remote +}; + +enum class RemoteMetadataStatus{ + UNINITIALIZED, + NOT_AVAILABLE, + AVAILABLE, +}; +struct RemoteMetadata { + RemoteMetadataStatus status = RemoteMetadataStatus::UNINITIALIZED; + DownloadedResourceMetadata metadata; +}; + + +const std::vector& local_resource_download_list(); +const std::vector& remote_resource_download_list(); +std::optional get_resource_version_num(Filesystem::Path folder_path); +ResourceVersionStatus get_version_status(uint16_t expected_version_num, std::optional current_version_num); + +} +#endif diff --git a/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadOptions.cpp b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadOptions.cpp new file mode 100644 index 0000000000..60fdb6cd84 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadOptions.cpp @@ -0,0 +1,46 @@ +/* Resource Download Row + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "ResourceDownloadRow.h" +#include "ResourceDownloadOptions.h" + +// #include + +#include +using std::cout; +using std::endl; + +namespace PokemonAutomation{ + +// ResourceDownloadButton::~ResourceDownloadButton(){} +ResourceDownloadButton::ResourceDownloadButton(ResourceDownloadRow& p_row) + : ConfigOptionImpl(LockMode::UNLOCK_WHILE_RUNNING) + , row(p_row) + , m_enabled(true) +{} + + +ResourceDeleteButton::ResourceDeleteButton(ResourceDownloadRow& p_row) + : ConfigOptionImpl(LockMode::UNLOCK_WHILE_RUNNING) + , row(p_row) + , m_enabled(true) +{} + + +ResourceCancelButton::ResourceCancelButton(ResourceDownloadRow& p_row) + : ConfigOptionImpl(LockMode::UNLOCK_WHILE_RUNNING) + , row(p_row) + , m_enabled(true) +{} + + +ResourceProgressBar::ResourceProgressBar(ResourceDownloadRow& p_row) + : ConfigOptionImpl(LockMode::UNLOCK_WHILE_RUNNING) + , row(p_row) +{} + + +} diff --git a/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadOptions.h b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadOptions.h new file mode 100644 index 0000000000..d8b934efc0 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadOptions.h @@ -0,0 +1,89 @@ +/* Resource Download Row + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_ResourceDownloadOptions_H +#define PokemonAutomation_ResourceDownloadOptions_H + +#include +// #include "Common/Cpp/Containers/Pimpl.h" +// #include "Common/Cpp/Concurrency/AsyncTask.h" +// #include "Common/Cpp/Options/StaticTableOption.h" +// #include "ResourceDownloadHelpers.h" + + +namespace PokemonAutomation{ + +class ResourceDownloadRow; + + +class ResourceDownloadButton : public QObject, public ConfigOptionImpl{ + Q_OBJECT +public: + // ~ResourceDownloadButton(); + ResourceDownloadButton(ResourceDownloadRow& p_row); + +public: + inline bool get_enabled(){ return m_enabled; } + inline void set_enabled(bool enabled){ + m_enabled = enabled; + } + +public: + ResourceDownloadRow& row; + +private: + bool m_enabled; // button should be blocked during an active task. m_enabled is false when blocked + + + + +}; + +class ResourceDeleteButton : public ConfigOptionImpl{ +public: + ResourceDeleteButton(ResourceDownloadRow& p_row); + +public: + inline bool get_enabled(){ return m_enabled; } + inline void set_enabled(bool enabled){ + m_enabled = enabled; + } + +public: + ResourceDownloadRow& row; + +private: + bool m_enabled; +}; + +class ResourceCancelButton : public ConfigOptionImpl{ +public: + ResourceCancelButton(ResourceDownloadRow& p_row); + +public: + inline bool get_enabled(){ return m_enabled; } + inline void set_enabled(bool enabled){ + m_enabled = enabled; + } + +public: + ResourceDownloadRow& row; + +private: + bool m_enabled; +}; + +class ResourceProgressBar : public ConfigOptionImpl{ +public: + ResourceProgressBar(ResourceDownloadRow& p_row); + + ResourceDownloadRow& row; +}; + + + +} +#endif diff --git a/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadRow.cpp b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadRow.cpp new file mode 100644 index 0000000000..9f5a76592d --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadRow.cpp @@ -0,0 +1,482 @@ +/* Resource Download Row + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonFramework/Globals.h" +#include "Common/Cpp/Containers/Pimpl.tpp" +#include "Common/Cpp/PrettyPrint.h" +// #include "Common/Cpp/Exceptions.h" +#include "CommonFramework/Tools/GlobalThreadPools.h" +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/Logging/Logger.h" +#include "CommonFramework/Tools/FileDownloader.h" +#include "CommonFramework/Tools/FileUnzip.h" +#include "CommonFramework/Tools/FileHash.h" +#include "Common/Cpp/Filesystem.h" +#include "CommonFramework/Options/LabelCellOption.h" +// #include "ResourceDownloadTable.h" +#include "ResourceDownloadRow.h" + +// #include +// #include +#include + +#include +using std::cout; +using std::endl; + +namespace PokemonAutomation{ + + namespace fs = std::filesystem; + +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// ResourceDownloadRow +///////////////////////////////////////////////////////////////////////////////////////////////////////// + + +std::string resource_version_to_string(ResourceVersionStatus version){ + switch(version){ + case ResourceVersionStatus::CURRENT: + return "Current"; + case ResourceVersionStatus::OUTDATED: + return "Outdated"; + case ResourceVersionStatus::NOT_APPLICABLE: + return "--"; + // case ResourceVersionStatus::BLANK: + // return ""; + case ResourceVersionStatus::FUTURE_VERSION: + return "Unsupported future version.
Please update the Computer Control program."; + default: + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "resource_version_to_string: Unknown enum."); + } +} + +std::string is_downloaded_string(bool is_downloaded){ + return is_downloaded ? "Yes" : "--"; +} + +struct ResourceDownloadRow::Data{ + Data( + std::string& resource_name, + size_t file_size, + bool is_downloaded, + std::optional version_num, + ResourceVersionStatus version_status + ) + : m_resource_name(LockMode::LOCK_WHILE_RUNNING, resource_name) + , m_file_size(file_size) + , m_file_size_label(LockMode::LOCK_WHILE_RUNNING, tostr_bytes(file_size)) + , m_is_downloaded(is_downloaded) + , m_is_downloaded_label(LockMode::LOCK_WHILE_RUNNING, is_downloaded_string(is_downloaded)) + , m_version_num(version_num) + , m_version_status(version_status) + , m_version_status_label(LockMode::LOCK_WHILE_RUNNING, resource_version_to_string(version_status)) + , m_cancel_action(false) + {} + + LabelCellOption m_resource_name; + + size_t m_file_size; + LabelCellOption m_file_size_label; + + bool m_is_downloaded; + LabelCellOption m_is_downloaded_label; + + std::optional m_version_num; + ResourceVersionStatus m_version_status; + LabelCellOption m_version_status_label; + + std::atomic m_cancel_action; + + +}; + +void ResourceDownloadRow::set_version_status(ResourceVersionStatus version_status){ + m_data->m_version_status = version_status; + m_data->m_version_status_label.set_text(resource_version_to_string(version_status)); +} + + +void ResourceDownloadRow::set_is_downloaded(bool is_downloaded){ + m_data->m_is_downloaded = is_downloaded; + m_data->m_is_downloaded_label.set_text(is_downloaded_string(is_downloaded)); +} + +void ResourceDownloadRow::set_cancel_action(bool cancel_action){ + m_data->m_cancel_action = cancel_action; +} + + +ResourceDownloadRow::~ResourceDownloadRow(){ + m_worker1.wait_and_ignore_exceptions(); + m_worker2.wait_and_ignore_exceptions(); + m_worker3.wait_and_ignore_exceptions(); +} +ResourceDownloadRow::ResourceDownloadRow( + DownloadedResourceMetadata local_metadata, + bool is_downloaded, + std::optional version_num, + ResourceVersionStatus version_status +) + : StaticTableRow(local_metadata.resource_name) + , m_button_state(ButtonState::READY) + , m_local_metadata(local_metadata) + , m_data(CONSTRUCT_TOKEN, local_metadata.resource_name, local_metadata.size_decompressed_bytes, is_downloaded, version_num, version_status) + , m_download_button(*this) + , m_delete_button(*this) + , m_cancel_button(*this) + , m_progress_bar(*this) +{ + PA_ADD_STATIC(m_data->m_resource_name); + PA_ADD_STATIC(m_data->m_file_size_label); + PA_ADD_STATIC(m_data->m_is_downloaded_label); + PA_ADD_STATIC(m_data->m_version_status_label); + + PA_ADD_STATIC(m_download_button); + PA_ADD_STATIC(m_delete_button); + PA_ADD_STATIC(m_cancel_button); + PA_ADD_STATIC(m_progress_bar); +} + + + +void ResourceDownloadRow::initialize_remote_metadata(){ + DownloadedResourceMetadata corresponding_remote_metadata; + RemoteMetadataStatus status = RemoteMetadataStatus::NOT_AVAILABLE; + std::vector all_remote_metadata = remote_resource_download_list(); + + std::string resource_name = m_data->m_resource_name.text(); + + for (DownloadedResourceMetadata remote_metadata : all_remote_metadata){ + if (remote_metadata.resource_name == resource_name){ + corresponding_remote_metadata = remote_metadata; + status = RemoteMetadataStatus::AVAILABLE; + break; + } + } + + RemoteMetadata remote_metadata = {status, corresponding_remote_metadata}; + + m_remote_metadata = std::make_unique(remote_metadata); +} + +RemoteMetadata& ResourceDownloadRow::fetch_remote_metadata(){ + // Only runs once per instance + std::call_once(init_flag, &ResourceDownloadRow::initialize_remote_metadata, this); + return *m_remote_metadata; +} + +// DownloadedResourceMetadata ResourceDownloadRow::initialize_local_metadata(){ +// DownloadedResourceMetadata corresponding_local_metadata; +// std::vector all_local_metadata = local_resource_download_list(); + +// std::string resource_name = m_data->m_resource_name.text(); + +// bool found = false; +// for (DownloadedResourceMetadata local_metadata : all_local_metadata){ +// if (local_metadata.resource_name == resource_name){ +// corresponding_local_metadata = local_metadata; +// found = true; +// break; +// } +// } + +// if (!found){ +// throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "initialize_local_metadata: Corresponding DownloadedResourceMetadata not found in the local JSON file."); +// } + +// return corresponding_local_metadata; +// } + + +void ResourceDownloadRow::ensure_remote_metadata_loaded(){ + m_worker1 = GlobalThreadPools::unlimited_normal().dispatch_now_blocking( + [this]{ + try { + // std::this_thread::sleep_for(std::chrono::seconds(1)); + std::string predownload_warning; + RemoteMetadata& remote_handle = fetch_remote_metadata(); + // cout << "Fetched remote metadata" << endl; + // throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "testing"); + + predownload_warning = predownload_warning_summary(remote_handle); + + // update_button_state(ButtonState::READY); + emit metadata_fetch_finished(predownload_warning); + + }catch(OperationFailedException&){ + // cout << "failed" << endl; + update_button_state(ButtonState::READY); + emit download_failed(); + return; + }catch(...){ + update_button_state(ButtonState::READY); + // cout << "Exception thrown in thread" << endl; + emit exception_caught("ResourceDownloadButton::ensure_remote_metadata_loaded"); + return; + } + + } + ); + +} + +std::string ResourceDownloadRow::predownload_warning_summary(RemoteMetadata& remote_handle){ + + std::string predownload_warning; + + switch (remote_handle.status){ + case RemoteMetadataStatus::UNINITIALIZED: + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "predownload_warning_summary: Remote metadata uninitialized."); + case RemoteMetadataStatus::NOT_AVAILABLE: + predownload_warning = "Resource no longer available for download. We recommend updating the Computer Control program."; + break; + case RemoteMetadataStatus::AVAILABLE: + { + uint16_t local_version_num = m_local_metadata.version_num.value(); + + DownloadedResourceMetadata remote_metadata = remote_handle.metadata; + uint16_t remote_version_num = remote_metadata.version_num.value(); + size_t compressed_size = remote_metadata.size_compressed_bytes; + size_t decompressed_size = remote_metadata.size_decompressed_bytes; + + std::string disk_space_requirement = "This will require " + tostr_bytes(decompressed_size + compressed_size) + " of free space"; + + if (local_version_num < remote_version_num){ + predownload_warning = "The resource you are downloading is a more updated version than the program expects. " + "This may or may not cause issues with the programs. " + "We recommend updating the Computer Control program.
" + + disk_space_requirement; + }else if (local_version_num == remote_version_num){ + predownload_warning = "Update available.
" + disk_space_requirement; + }else if (local_version_num > remote_version_num){ + predownload_warning = "The resource you are downloading is a less updated version than the program expects. " + "Please report this as a bug.
" + + disk_space_requirement; + } + } + break; + default: + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "predownload_warning_summary: Unknown enum."); + } + + return predownload_warning; +} + + + +void ResourceDownloadRow::start_download(){ + m_worker2 = GlobalThreadPools::unlimited_normal().dispatch_now_blocking( + [this]{ + try { + + // std::this_thread::sleep_for(std::chrono::seconds(7)); + RemoteMetadata& remote_handle = fetch_remote_metadata(); + if (remote_handle.status != RemoteMetadataStatus::AVAILABLE){ + switch (remote_handle.status){ + case RemoteMetadataStatus::UNINITIALIZED: + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "start_download: Remote metadata uninitialized."); + case RemoteMetadataStatus::NOT_AVAILABLE: + cout << "start_download: Download not available. Cancel download." << endl; + throw OperationCancelledException(); + default: + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "start_download: Unknown enum."); + } + } + + // Download is available + DownloadedResourceMetadata metadata = remote_handle.metadata; + run_download(metadata); + + cout << "Done Download" << endl; + + update_button_state(ButtonState::READY); + + }catch(OperationCancelledException&){ + update_button_state(ButtonState::READY); + return; + }catch(OperationFailedException&){ + update_button_state(ButtonState::READY); + emit download_failed(); + return; + }catch(...){ + update_button_state(ButtonState::READY); + emit exception_caught("ResourceDownloadButton::start_download"); + return; + } + } + ); + +} + + +void ResourceDownloadRow::run_download(DownloadedResourceMetadata resource_metadata){ + Logger& logger = global_logger_tagged(); + // std::this_thread::sleep_for(std::chrono::seconds(5)); + + std::string url = resource_metadata.url; + std::string resource_name = resource_metadata.resource_name; + qint64 expected_size = resource_metadata.size_compressed_bytes; + + std::string resource_directory = DOWNLOADED_RESOURCE_PATH() + resource_name; + try{ + // delete directory and the old resource + fs::remove_all(Filesystem::Path(resource_directory)); + + // download + std::string zip_path = resource_directory + "/temp.zip"; + FileDownloader::download_file_to_disk( + logger, + url, + zip_path, + expected_size, + [this](int percentage_progress){ + download_progress(percentage_progress); + }, + [this](){ + return m_data->m_cancel_action.load(); + } + ); + + // hash + std::string hash = + hash_file( + zip_path, + [this](int percentage_progress){ + hash_progress(percentage_progress); + } + ); + std::string expected_hash = resource_metadata.sha_256; + if (hash != expected_hash){ + std::cerr << "current hash: " << hash << endl; + throw_and_log(logger, ErrorReport::NO_ERROR_REPORT, + "Downloaded file failed verification. SHA 256 hash did not match the expected value."); + } + + // Filesystem::Path p{zip_path}; + // cout << "File size: " << std::filesystem::file_size(p) << endl; + + // unzip + unzip_file( + zip_path.c_str(), + resource_directory.c_str(), + [this](int percentage_progress){ + unzip_progress(percentage_progress); + } + , + [this](){ + return m_data->m_cancel_action.load(); + } + ); + + // delete old zip file + fs::remove(Filesystem::Path(zip_path)); + + if (m_data->m_cancel_action.load()){ + throw OperationCancelledException(); + } + + // update the table labels + set_is_downloaded(true); + set_version_status(ResourceVersionStatus::CURRENT); + }catch(OperationCancelledException& e){ + // delete directory and the resource + fs::remove_all(Filesystem::Path(resource_directory)); + + // update the table labels + set_is_downloaded(false); + set_version_status(ResourceVersionStatus::NOT_APPLICABLE); + + throw e; + }catch(...){ + // delete directory and the resource + fs::remove_all(Filesystem::Path(resource_directory)); + + // update the table labels + set_is_downloaded(false); + set_version_status(ResourceVersionStatus::NOT_APPLICABLE); + + throw; + } + +} + + +void ResourceDownloadRow::start_delete(){ + m_worker3 = GlobalThreadPools::unlimited_normal().dispatch_now_blocking( + [this]{ + try { + std::string resource_name = m_local_metadata.resource_name; + + std::string resource_directory = DOWNLOADED_RESOURCE_PATH() + resource_name; + // delete directory and the old resource + fs::remove_all(Filesystem::Path(resource_directory)); + + // update the table labels + set_is_downloaded(false); + set_version_status(ResourceVersionStatus::NOT_APPLICABLE); + + update_button_state(ButtonState::READY); + }catch(...){ + update_button_state(ButtonState::READY); + emit exception_caught("ResourceDownloadButton::start_delete"); + return; + } + } + ); + +} + +void ResourceDownloadRow::update_button_state(ButtonState state){ + switch (state){ + case ButtonState::DOWNLOAD: + // button state can only enter the DOWNLOAD state + // if going from the READY state + if (m_button_state == ButtonState::READY){ + m_download_button.set_enabled(false); + m_delete_button.set_enabled(false); + m_cancel_button.set_enabled(true); + set_cancel_action(false); + m_button_state = state; + } + break; + case ButtonState::DELETE: + // button state can only enter the DELETE state + // if going from the READY state + if (m_button_state == ButtonState::READY){ + m_download_button.set_enabled(false); + m_delete_button.set_enabled(false); + m_cancel_button.set_enabled(false); + set_cancel_action(false); + m_button_state = state; + } + break; + case ButtonState::CANCEL: + // button state can only enter the CANCEL state + // if going from the DOWNLOAD state + if (m_button_state == ButtonState::DOWNLOAD){ + m_download_button.set_enabled(false); + m_delete_button.set_enabled(false); + m_cancel_button.set_enabled(false); + set_cancel_action(true); + m_button_state = state; + } + break; + case ButtonState::READY: + m_download_button.set_enabled(true); + m_delete_button.set_enabled(true); + m_cancel_button.set_enabled(true); + set_cancel_action(false); + m_button_state = state; + break; + default: + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "update_button_state: Unknown enum."); + } + + emit button_state_updated(); +} + + +} diff --git a/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadRow.h b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadRow.h new file mode 100644 index 0000000000..937ee3d0fb --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadRow.h @@ -0,0 +1,98 @@ +/* Resource Download Row + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_ResourceDownloadRow_H +#define PokemonAutomation_ResourceDownloadRow_H + +#include +#include "Common/Cpp/Containers/Pimpl.h" +#include "Common/Cpp/Concurrency/AsyncTask.h" +// #include "CommonFramework/Tools/GlobalThreadPools.h" +#include "Common/Cpp/Options/StaticTableOption.h" +#include "ResourceDownloadHelpers.h" +#include "ResourceDownloadOptions.h" +#include + +namespace PokemonAutomation{ + +enum class ButtonState{ + DOWNLOAD, + DELETE, + CANCEL, + READY, +}; +class ResourceDownloadRow : public QObject, public StaticTableRow{ + Q_OBJECT +public: + ~ResourceDownloadRow(); + ResourceDownloadRow( + DownloadedResourceMetadata local_metadata, + bool is_downloaded, + std::optional version_num, + ResourceVersionStatus version_status + ); + +signals: + void download_progress(int percentage); + void unzip_progress(int percentage); + void hash_progress(int percentage); + + void metadata_fetch_finished(std::string popup_message); + void exception_caught(std::string function_name); + void download_failed(); + void download_finished(); + + void button_state_updated(); + + +public: + void set_version_status(ResourceVersionStatus version_status); + void set_is_downloaded(bool is_downloaded); + void set_cancel_action(bool cancel_action); + + void ensure_remote_metadata_loaded(); + std::string predownload_warning_summary(RemoteMetadata& remote_metadata); + // get the DownloadedResourceMetadata from the remote JSON, that corresponds to this button/row + void initialize_remote_metadata(); + RemoteMetadata& fetch_remote_metadata(); + // DownloadedResourceMetadata initialize_local_metadata(); + + void start_download(); + // throws OperationCancelledException if the user cancels the action + void run_download(DownloadedResourceMetadata resource_metadata); + + void start_delete(); + + void update_button_state(ButtonState state); + + inline ButtonState get_button_state(){ return m_button_state; } + + +private: + std::once_flag init_flag; + std::unique_ptr m_remote_metadata; + + ButtonState m_button_state; + DownloadedResourceMetadata m_local_metadata; + struct Data; + Pimpl m_data; + + ResourceDownloadButton m_download_button; + ResourceDeleteButton m_delete_button; + ResourceCancelButton m_cancel_button; + ResourceProgressBar m_progress_bar; + + AsyncTask m_worker1; + AsyncTask m_worker2; + AsyncTask m_worker3; + + + + +}; + +} +#endif diff --git a/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadTable.cpp b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadTable.cpp new file mode 100644 index 0000000000..6a2187241a --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadTable.cpp @@ -0,0 +1,124 @@ +/* Resource Download Table + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonFramework/Globals.h" +#include "Common/Cpp/Exceptions.h" +// #include "CommonFramework/Logging/Logger.h" +// #include "CommonFramework/Tools/GlobalThreadPools.h" +// #include "CommonFramework/Tools/FileDownloader.h" +// #include "CommonFramework/Exceptions/OperationFailedException.h" +// #include "Common/Cpp/Json/JsonArray.h" +// #include "Common/Cpp/Json/JsonObject.h" +#include "Common/Cpp/Filesystem.h" +#include "ResourceDownloadRow.h" +#include "ResourceDownloadTable.h" + +// #include +// #include +// #include + +#include +using std::cout; +using std::endl; + +namespace PokemonAutomation{ + + +std::vector> get_resource_download_rows(){ + std::vector> resource_rows; + std::vector resource_list; + try{ + resource_list = local_resource_download_list(); + }catch(FileException&){ + return {}; + } + + for (const DownloadedResourceMetadata& resource : resource_list){ + std::string resource_name = resource.resource_name; + uint16_t expected_version_num = resource.version_num.value(); + std::optional current_version_num; // default nullopt + + Filesystem::Path filepath{DOWNLOADED_RESOURCE_PATH() + resource_name}; + bool is_downloaded = std::filesystem::is_directory(filepath); + if (is_downloaded){ + current_version_num = get_resource_version_num(filepath); + } + + ResourceVersionStatus version_status = get_version_status(expected_version_num, current_version_num); + + resource_rows.emplace_back(std::make_unique(resource, is_downloaded, current_version_num, version_status)); + } + + return resource_rows; +} + + + + +ResourceDownloadTable::~ResourceDownloadTable(){ + m_worker.wait_and_ignore_exceptions(); +} + +ResourceDownloadTable::ResourceDownloadTable() + : StaticTableOption("Resource Downloading:
Download resources not included in the initial download of the program.", LockMode::LOCK_WHILE_RUNNING, false) + , m_resource_rows(get_resource_download_rows()) +{ + add_resource_download_rows(); + + finish_construction(); +} +std::vector ResourceDownloadTable::make_header() const{ + std::vector ret{ + "Resource", + "Size", + "Downloaded", + "Version", + "", + "", + "", + "", + }; + return ret; +} + +// UiWrapper ResourceDownloadTable::make_UiComponent(void* params) { +// m_worker = GlobalThreadPools::unlimited_normal().dispatch_now_blocking( +// [this]{ +// check_all_resource_versions(); +// } +// ); + +// return ConfigOptionImpl::make_UiComponent(params); +// } + +void ResourceDownloadTable::add_resource_download_rows(){ + for (std::unique_ptr& row_ptr : m_resource_rows){ + add_row(row_ptr.get()); + } +} + + + + +// void ResourceDownloadTable::check_all_resource_versions(){ +// std::vector remote_resources = remote_resource_download_list(); + + + +// // const JsonArray& resource_list = json_obj.get_array_throw("resourceList"); + +// // test code +// std::this_thread::sleep_for(std::chrono::seconds(5)); + +// for (auto& row_ptr : m_resource_rows){ +// row_ptr->m_data->m_version_status_label.set_text("Hi"); +// } + +// } + + + +} diff --git a/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadTable.h b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadTable.h new file mode 100644 index 0000000000..c54cdef985 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadTable.h @@ -0,0 +1,41 @@ +/* Resource Download Table + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_ResourceDownloadTable_H +#define PokemonAutomation_ResourceDownloadTable_H + +#include "Common/Cpp/Concurrency/AsyncTask.h" +#include "Common/Cpp/Options/StaticTableOption.h" +// #include "ResourceDownloadRow.h" + +namespace PokemonAutomation{ + +class ResourceDownloadRow; + +class ResourceDownloadTable : public StaticTableOption{ +public: + ~ResourceDownloadTable(); + ResourceDownloadTable(); + + virtual std::vector make_header() const override; + // virtual UiWrapper make_UiComponent(void* params) override; + +private: + void add_resource_download_rows(); + + +private: + // we need to keep a handle on each Row, so that we can edit m_is_downloaded_label later on. + std::vector> m_resource_rows; + + AsyncTask m_worker; + +}; + + + +} +#endif diff --git a/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadWidget.cpp b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadWidget.cpp new file mode 100644 index 0000000000..97f2d670a4 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadWidget.cpp @@ -0,0 +1,435 @@ +/* Resource Download Widget + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include +#include +#include "CommonFramework/Logging/Logger.h" +#include "Common/Cpp/Exceptions.h" + +#include "CommonFramework/Notifications/ProgramNotifications.h" +#include "ResourceDownloadWidget.h" + +#include +using std::cout; +using std::endl; + +namespace PokemonAutomation{ + +void show_error_box(std::string function_name){ + std::cerr << "Error: Exception thrown in thread. From " + function_name + ". Report this as a bug." << std::endl; + QMessageBox box; + box.warning(nullptr, "Error:", + QString::fromStdString("Error: Exception thrown in thread. From " + function_name + ". Report this as a bug.")); + +} + +void show_download_failed_box(){ + std::cerr << "Error: Download failed. Check your internet connection and check you have enough disk space." << std::endl; + QMessageBox box; + box.warning(nullptr, "Error:", + QString::fromStdString("Error: Download failed. Check your internet connection and check you have enough disk space.")); + +} + + +template class RegisterConfigWidget; +DownloadButtonWidget::~DownloadButtonWidget(){ + // cout << "Destructor for DownloadButtonWidget" << endl; + // m_value.disconnect(this); +} +DownloadButtonWidget::DownloadButtonWidget(QWidget& parent, ResourceDownloadButton& value) + : QWidget(&parent) + , ConfigWidget(value, *this) + , m_value(value) +{ + m_button = new QPushButton(&parent); + m_widget = m_button; + + // cout << "Constructor for DownloadButtonWidget" << endl; + + QFont font; + font.setBold(true); + m_button->setFont(font); + + QFontMetrics metrics(m_button->font()); + int minWidth = metrics.horizontalAdvance("Downloading..."); + m_button->setMinimumWidth(minWidth); + + // Button should be disabled when in the middle of downloading + // this status is stored within ResourceDownloadButton::m_enabled + // when the button is clicked, m_enabled is set to false + // when te download is done, m_enabled is set back to true + // the UI is updated to reflect the status of m_enabled, by using update_UI_state + + + // update the UI based on m_enabled, when the button is constructed + update_UI_state(); + + // when the button is clicked, runs row.update_button_state(), which updates the button state + // also, fetch json + connect( + m_button, &QPushButton::clicked, + this, [this](){ + m_value.row.update_button_state(ButtonState::DOWNLOAD); + m_value.row.ensure_remote_metadata_loaded(); + } + ); + + // when button_state_updated, update the UI state to match + connect( + &m_value.row, &ResourceDownloadRow::button_state_updated, + this, [this](){ + update_UI_state(); + } + ); + + // when json has been fetched, open the update box. + // When click Ok in update box, start the download. If click cancel, re-enable the download button + connect( + &m_value.row, &ResourceDownloadRow::metadata_fetch_finished, + this, [this](std::string predownload_warning){ + show_download_confirm_box("Download", predownload_warning); + } + ); + + // if the thread catches an exception, show an error box + // since exceptions can't bubble up as usual + // this connect handles all exception_caught() emitted by ResourceDownloadRow + connect( + &m_value.row, &ResourceDownloadRow::exception_caught, + this, [](std::string function_name){ + show_error_box(function_name); + } + ); + + // if download fails + connect( + &m_value.row, &ResourceDownloadRow::download_failed, + this, [](){ + show_download_failed_box(); + } + ); +} + + +void DownloadButtonWidget::update_UI_state(){ + if (m_value.get_enabled()){ + m_button->setEnabled(true); + m_button->setText("Download"); + }else{ + m_button->setEnabled(false); + if (m_value.row.get_button_state() == ButtonState::DOWNLOAD){ + m_button->setText("Downloading..."); + } + } +} + + +void DownloadButtonWidget::show_download_confirm_box( + const std::string& title, + const std::string& message_body +){ + QMessageBox box; + QPushButton* ok = box.addButton(QMessageBox::Ok); + QPushButton* cancel = box.addButton("Cancel", QMessageBox::NoRole); + box.setEscapeButton(cancel); +// cout << "ok = " << ok << endl; +// cout << "skip = " << skip << endl; + + box.setTextFormat(Qt::RichText); + std::string text = message_body; + // text += make_text_url(link_url, link_text); + // text += get_changes(node); + + + box.setWindowTitle(QString::fromStdString(title)); + box.setText(QString::fromStdString(text)); + +// box.open(); + + box.exec(); + + QAbstractButton* clicked = box.clickedButton(); +// cout << "clicked = " << clicked << endl; + if (clicked == ok){ + cout << "Clicked Ok to Download" << endl; + + m_value.row.start_download(); + return; + } + if (clicked == cancel){ + m_value.row.update_button_state(ButtonState::READY); + return; + } +} + + + + + +template class RegisterConfigWidget; +DeleteButtonWidget::DeleteButtonWidget(QWidget& parent, ResourceDeleteButton& value) + : QWidget(&parent) + , ConfigWidget(value, *this) + , m_value(value) +{ + m_button = new QPushButton(&parent); + m_widget = m_button; + + QFont font; + font.setBold(true); + m_button->setFont(font); + m_button->setText("Delete"); + + QFontMetrics metrics(m_button->font()); + int minWidth = metrics.horizontalAdvance("Deleting..."); + m_button->setMinimumWidth(minWidth); + + + // update the UI based on m_enabled, when the button is constructed + update_UI_state(); + + // when the button is clicked, runs row.update_button_state(), which updates the button state + // also, show the delete confirm box + connect( + m_button, &QPushButton::clicked, + this, [&](bool){ + m_value.row.update_button_state(ButtonState::DELETE); + show_delete_confirm_box(); + cout << "Clicked Delete Button" << endl; + } + ); + + // when button_state_updated, update the UI state to match + connect( + &m_value.row, &ResourceDownloadRow::button_state_updated, + this, [this](){ + update_UI_state(); + } + ); + +} + + +void DeleteButtonWidget::update_UI_state(){ + if (m_value.get_enabled()){ + m_button->setEnabled(true); + m_button->setText("Delete"); + }else{ + m_button->setEnabled(false); + if (m_value.row.get_button_state() == ButtonState::DELETE){ + m_button->setText("Deleting..."); + } + } +} + + +void DeleteButtonWidget::show_delete_confirm_box(){ + QMessageBox box; + QPushButton* yes = box.addButton(QMessageBox::Yes); + QPushButton* cancel = box.addButton("Cancel", QMessageBox::NoRole); + box.setEscapeButton(cancel); +// cout << "ok = " << ok << endl; +// cout << "skip = " << skip << endl; + + box.setTextFormat(Qt::RichText); + std::string title = "Delete"; + std::string message_body = "Are you sure you want to delete this resource?"; + + box.setWindowTitle(QString::fromStdString(title)); + box.setText(QString::fromStdString(message_body)); + +// box.open(); + + box.exec(); + + QAbstractButton* clicked = box.clickedButton(); +// cout << "clicked = " << clicked << endl; + if (clicked == yes){ + cout << "Clicked Yes to Delete" << endl; + + m_value.row.start_delete(); + return; + } + if (clicked == cancel){ + m_value.row.update_button_state(ButtonState::READY); + return; + } +} + +template class RegisterConfigWidget; +CancelButtonWidget::CancelButtonWidget(QWidget& parent, ResourceCancelButton& value) + : QWidget(&parent) + , ConfigWidget(value, *this) + , m_value(value) +{ + m_button = new QPushButton(&parent); + m_widget = m_button; + + QFont font; + font.setBold(true); + m_button->setFont(font); + m_button->setText("Cancel"); + + QFontMetrics metrics(m_button->font()); + int minWidth = metrics.horizontalAdvance("Cancelling..."); + m_button->setMinimumWidth(minWidth); + + // update the UI based on m_enabled, when the button is constructed + update_UI_state(); + + // when the button is clicked, runs row.update_button_state(), which updates the button state + // also, set cancel state to true + connect( + m_button, &QPushButton::clicked, + this, [&](bool){ + m_value.row.update_button_state(ButtonState::CANCEL); + cout << "Clicked Cancel Button" << endl; + } + ); + + // when button_state_updated, update the UI state to match + connect( + &m_value.row, &ResourceDownloadRow::button_state_updated, + this, [this](){ + update_UI_state(); + } + ); + +} + +void CancelButtonWidget::update_UI_state(){ + if (m_value.get_enabled()){ + m_button->setEnabled(true); + m_button->setText("Cancel"); + }else{ + m_button->setEnabled(false); + if (m_value.row.get_button_state() == ButtonState::CANCEL){ + m_button->setText("Cancelling..."); + } + } +} + +template class RegisterConfigWidget; +ProgressBarWidget::~ProgressBarWidget(){ + // m_value.row.disconnect(this); + // cout << "Destructor for ProgressBarWidget" << endl; + +} +ProgressBarWidget::ProgressBarWidget(QWidget& parent, ResourceProgressBar& value) + : QWidget(&parent) + , ConfigWidget(value, *this) + , m_value(value) +{ + + // 1. Instantiate the widgets + m_status_label = new QLabel("", this); + m_progress_bar = new QProgressBar(this); + + // cout << "Constructor for ProgressBarWidget" << endl; + + // 2. Configure the progress bar + m_progress_bar->setRange(0, 100); + m_progress_bar->setValue(0); + m_progress_bar->setTextVisible(true); // Shows % inside the bar + m_progress_bar->hide(); + + // 3. Create a horizontal layout to hold them + QHBoxLayout *layout = new QHBoxLayout(); + layout->addWidget(m_status_label); + layout->addWidget(m_progress_bar); + + this->setLayout(layout); + this->setMinimumWidth(170); + + connect( + &m_value.row, &ResourceDownloadRow::download_progress, + this, + [this](int percentage_progress){ + if (m_progress_bar->isHidden()) { + m_progress_bar->show(); // Make it visible when progress starts + } + m_status_label->setText("Downloading"); + m_progress_bar->setValue(percentage_progress); + // Simple Console Progress Bar + // std::cout << "\rProgress: [" << std::string(percentage_progress / 5, '#') + // << std::string(20 - (percentage_progress / 5), ' ') << "] " + // << percentage_progress << "%" << endl; + + } + ); + + + connect( + &m_value.row, &ResourceDownloadRow::unzip_progress, + this, + [this](int percentage_progress){ + if (m_progress_bar->isHidden()) { + m_progress_bar->show(); // Make it visible when progress starts + } + m_status_label->setText("Unzipping"); + m_progress_bar->setValue(percentage_progress); + } + ); + + connect( + &m_value.row, &ResourceDownloadRow::hash_progress, + this, + [this](int percentage_progress){ + if (m_progress_bar->isHidden()) { + m_progress_bar->show(); // Make it visible when progress starts + } + m_status_label->setText("Verifying"); + m_progress_bar->setValue(percentage_progress); + } + ); + + // when button_state_updated, update the UI state to match + connect( + &m_value.row, &ResourceDownloadRow::button_state_updated, + this, [this](){ + update_UI_state(); + } + ); + +} + + +void ProgressBarWidget::update_UI_state(){ + ButtonState state = m_value.row.get_button_state(); + switch (state){ + case ButtonState::DOWNLOAD: + m_status_label->setText("Downloading"); + if (m_progress_bar->isHidden()) { + m_progress_bar->show(); + } + break; + case ButtonState::DELETE: + // m_status_label->setText(""); + // m_progress_bar->hide(); + m_progress_bar->setValue(0); + break; + case ButtonState::CANCEL: + // m_status_label->setText(""); + // m_progress_bar->hide(); + m_progress_bar->setValue(0); + break; + case ButtonState::READY: + m_status_label->setText(""); + m_progress_bar->hide(); + m_progress_bar->setValue(0); + break; + default: + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "update_UI_state: Unknown enum."); + } +} + + +} diff --git a/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadWidget.h b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadWidget.h new file mode 100644 index 0000000000..86f4d757af --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadWidget.h @@ -0,0 +1,99 @@ +/* Resource Download Widget + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_ResourceDownloadWidget_H +#define PokemonAutomation_ResourceDownloadWidget_H + +#include +#include +#include +#include "Common/Qt/Options/ConfigWidget.h" +// #include "ResourceDownloadTable.h" +#include "ResourceDownloadRow.h" + +namespace PokemonAutomation{ + +// class ResourceDownloadButton; + +class DownloadButtonWidget : public QWidget, public ConfigWidget{ + Q_OBJECT +public: + using ParentOption = ResourceDownloadButton; + +public: + ~DownloadButtonWidget(); + DownloadButtonWidget(QWidget& parent, ResourceDownloadButton& value); + + +private: + void update_UI_state(); + void show_download_confirm_box( + const std::string& title, + const std::string& message_body + ); + +private: + ResourceDownloadButton& m_value; + QPushButton* m_button; + +}; + +void show_error_box(std::string function_name); + + +class DeleteButtonWidget : public QWidget, public ConfigWidget{ +public: + using ParentOption = ResourceDeleteButton; + +public: + DeleteButtonWidget(QWidget& parent, ResourceDeleteButton& value); + +private: + void update_UI_state(); + void show_delete_confirm_box(); + +private: + ResourceDeleteButton& m_value; + QPushButton* m_button; +}; + +class CancelButtonWidget : public QWidget, public ConfigWidget{ +public: + using ParentOption = ResourceCancelButton; + +public: + CancelButtonWidget(QWidget& parent, ResourceCancelButton& value); + +private: + void update_UI_state(); + +private: + ResourceCancelButton& m_value; + QPushButton* m_button; +}; + +class ProgressBarWidget : public QWidget, public ConfigWidget{ +public: + using ParentOption = ResourceProgressBar; + +public: + ~ProgressBarWidget(); + ProgressBarWidget(QWidget& parent, ResourceProgressBar& value); + +private: + void update_UI_state(); + +private: + ResourceProgressBar& m_value; + QLabel* m_status_label; + QProgressBar* m_progress_bar; +}; + + + + +} +#endif diff --git a/SerialPrograms/Source/CommonFramework/Tools/FileDownloader.cpp b/SerialPrograms/Source/CommonFramework/Tools/FileDownloader.cpp index 84f915e9b7..b441be9ed8 100644 --- a/SerialPrograms/Source/CommonFramework/Tools/FileDownloader.cpp +++ b/SerialPrograms/Source/CommonFramework/Tools/FileDownloader.cpp @@ -14,6 +14,9 @@ #include #include #include +#include +#include +#include #include "Common/Cpp/Json/JsonValue.h" #include "CommonFramework/Exceptions/OperationFailedException.h" #include "FileDownloader.h" @@ -89,6 +92,95 @@ std::string download_file(Logger& logger, const std::string& url){ return std::string(downloaded_data.data(), downloaded_data.size()); } + +void download_file_to_disk( + Logger& logger, + const std::string& url, + const std::string& file_path, + qint64 expected_size, + std::function progress_callback, + std::function is_cancelled +){ +// cout << "download_file()" << endl; + QNetworkAccessManager network_access_manager; + QEventLoop loop; + + // ensure the directory exists + QString filePath = QString::fromStdString(file_path); + QFileInfo fileInfo(filePath); + QString dirPath = fileInfo.absolutePath(); + QDir().mkpath(dirPath); + + // 1. Initialize QSaveFile + QSaveFile file(QString::fromStdString(file_path)); + if (!file.open(QIODevice::WriteOnly)) { + throw_and_log(logger, ErrorReport::NO_ERROR_REPORT, + "Could not open save file: " + file_path); + } + + QNetworkRequest request(QUrl(QString::fromStdString(url))); + // request.setAttribute(QNetworkRequest::AutoRedirectionPolicyAttribute, true); // enable auto-redirects + request.setTransferTimeout(std::chrono::seconds(5)); + + // 2. Start the GET request + QNetworkReply* reply = network_access_manager.get(request); + + // Progress Bar Logic. and check for Cancel + QObject::connect(reply, &QNetworkReply::downloadProgress, + [reply, expected_size, progress_callback, is_cancelled](qint64 bytesReceived, qint64 bytesTotal) { + + if (is_cancelled()){ + reply->abort(); + } + + // Use expected_size if the network doesn't provide one + qint64 total = (bytesTotal > 0) ? bytesTotal : expected_size; + + int percentage_progress = (total > 0) ? static_cast((bytesReceived * 100) / total) : 0; + progress_callback(std::min(percentage_progress, 100)); + } + ); + + // 3. Stream chunks directly to the temporary file + QObject::connect(reply, &QNetworkReply::readyRead, [&file, reply]() { + file.write(reply->readAll()); + }); + + // 4. Handle completion and errors + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + + // Start the loop. local wait mechanism that pauses execution of the function + // while Qt handles the network request. + // the loop stops once we see the signal QNetworkReply::finished. + loop.exec(); + + // // Final check for remaining data + if (reply->bytesAvailable() > 0) { + file.write(reply->readAll()); + } + + // 5. Finalize the transaction + if (reply->error() == QNetworkReply::NoError) { + // This moves the temporary file to the final destination 'file_path' + if (!file.commit()) { + throw_and_log(logger, ErrorReport::NO_ERROR_REPORT, + "Failed to commit file to disk: " + file_path); + } + } else { + if (is_cancelled()){ + logger.log("Download cancelled by user."); + throw OperationCancelledException(); + }else{ + QString error_string = reply->errorString(); + // QSaveFile automatically deletes the temp file if commit() isn't called + throw_and_log(logger, ErrorReport::NO_ERROR_REPORT, + "Network Error: " + error_string.toStdString()); + } + } + + reply->deleteLater(); +} + JsonValue download_json_file(Logger& logger, const std::string& url){ std::string downloaded_data = download_file(logger, url); return parse_json(downloaded_data); diff --git a/SerialPrograms/Source/CommonFramework/Tools/FileDownloader.h b/SerialPrograms/Source/CommonFramework/Tools/FileDownloader.h index 92243a3718..43b0eda9c0 100644 --- a/SerialPrograms/Source/CommonFramework/Tools/FileDownloader.h +++ b/SerialPrograms/Source/CommonFramework/Tools/FileDownloader.h @@ -8,6 +8,8 @@ #define PokemonAutomation_FileDownloader_H #include +#include +#include namespace PokemonAutomation{ class Logger; @@ -17,6 +19,16 @@ namespace FileDownloader{ // Throws OperationFailedException if failed to download. std::string download_file(Logger& logger, const std::string& url); +// Throws OperationFailedException if failed to download. +void download_file_to_disk( + Logger& logger, + const std::string& url, + const std::string& file_path, + qint64 expected_size, + std::function progress_callback, + std::function is_cancelled +); + // Throws OperationFailedException if failed to download. // Returns empty value if invalid JSON. JsonValue download_json_file(Logger& logger, const std::string& url); diff --git a/SerialPrograms/Source/CommonFramework/Tools/FileHash.cpp b/SerialPrograms/Source/CommonFramework/Tools/FileHash.cpp new file mode 100644 index 0000000000..7342287c0b --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Tools/FileHash.cpp @@ -0,0 +1,59 @@ +/* File Hash + * + * From: https://github.com/PokemonAutomation/ + * + */ + + + + + +#include "Common/Cpp/Exceptions.h" +#include +#include +#include + + + +#include +using std::cout; +using std::endl; + +namespace PokemonAutomation{ + + + +std::string hash_file(const std::string& file_path, std::function hash_progress) { + QFile file(QString::fromStdString(file_path)); + if (!file.open(QIODevice::ReadOnly)) { + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "hash_file: Could not open file."); + } + + QCryptographicHash hash(QCryptographicHash::Sha256); + qint64 file_size = file.size(); + qint64 total_bytes_read = 0; + + QByteArray buffer(1024 * 1024, 0); // Pre-allocate 1MB once + int last_percentage = -1; + while (!file.atEnd()) { + qint64 num_bytes_in_chunk = file.read(buffer.data(), buffer.size()); + if (num_bytes_in_chunk == -1) { + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "hash_file: Read error:" + file.errorString().toStdString()); + } + + hash.addData(QByteArrayView(buffer.data(), num_bytes_in_chunk)); + total_bytes_read += num_bytes_in_chunk; + + double percent = (static_cast(total_bytes_read) / file_size) * 100.0; + int current_percent = static_cast(percent); + // Only trigger callback if the integer value has changed + if (current_percent > last_percentage){ + hash_progress(current_percent); + last_percentage = current_percent; + } + } + + return hash.result().toHex().toStdString(); +} + +} diff --git a/SerialPrograms/Source/CommonFramework/Tools/FileHash.h b/SerialPrograms/Source/CommonFramework/Tools/FileHash.h new file mode 100644 index 0000000000..b2c9245b8f --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/Tools/FileHash.h @@ -0,0 +1,19 @@ +/* File Hash + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_FileHash_H +#define PokemonAutomation_FileHash_H + +#include +#include + +namespace PokemonAutomation{ + +// uses SHA 256 +std::string hash_file(const std::string& file_path, std::function hash_progress); + +} +#endif diff --git a/SerialPrograms/Source/CommonFramework/Tools/FileUnzip.cpp b/SerialPrograms/Source/CommonFramework/Tools/FileUnzip.cpp index c56fd9ccf9..a0eef27369 100644 --- a/SerialPrograms/Source/CommonFramework/Tools/FileUnzip.cpp +++ b/SerialPrograms/Source/CommonFramework/Tools/FileUnzip.cpp @@ -26,178 +26,220 @@ using std::endl; namespace PokemonAutomation{ - namespace fs = std::filesystem; - - struct ProgressData { - std::ofstream* out_file; - uint64_t total_bytes; - uint64_t processed_bytes; - int last_percentage; - }; - - // Callback triggered for every chunk of decompressed data - // pOpaque is an opaque pointer that actually represents ProgressData - size_t write_callback(void* pOpaque, mz_uint64 file_ofs, const void* pBuf, size_t n){ - ProgressData* data = static_cast(pOpaque); - - // 1. Check if we actually need to seek - // tellp() returns the current 'put' position. get the current position of the write pointer in an output stream. - if (static_cast(data->out_file->tellp()) != file_ofs){ - data->out_file->seekp(file_ofs); - } - - // Write chunk to disk - data->out_file->write(static_cast(pBuf), n); - - // Update and display progress - data->processed_bytes += n; - double percent = (double)data->processed_bytes / data->total_bytes * 100.0; - int current_percent = static_cast(percent); - - // Only print if the integer value has changed - if (current_percent > data->last_percentage){ - data->last_percentage = current_percent; - std::cout << "\rProgress: " << current_percent << "% (" - << data->processed_bytes << "/" << data->total_bytes << " bytes)" << endl; - } - - return n; +namespace fs = std::filesystem; + +struct ProgressData { + std::ofstream* out_file; + uint64_t total_bytes; + uint64_t processed_bytes; + int last_percentage; + std::function progress_callback; + std::function is_cancelled; +}; + +// Callback triggered for every chunk of decompressed data +// pOpaque is an opaque pointer that actually represents ProgressData +size_t write_callback(void* pOpaque, [[maybe_unused]] mz_uint64 file_ofs, const void* pBuf, size_t n){ + ProgressData* data = static_cast(pOpaque); + + if (data->is_cancelled()){ + return 0; // this causes mz_zip_reader_extract_to_callback to return an error } - // ensure that entry_name is inside target_dir, to prevent path traversal attacks. - bool is_safe(const std::string& target_dir, const std::string& entry_name){ - try { - // 1. Get absolute, normalized paths - // handles symlinks. and resolves .. and . components. throws error if path doesn't exist - Filesystem::Path base = fs::canonical(Filesystem::Path(target_dir)); - // confirms that base is a directory, and not a file - if (!fs::is_directory(base)) return false; + // Check if we actually need to seek + // tellp() returns the current 'put' position. get the current position of the write pointer in an output stream. + // if (static_cast(data->out_file->tellp()) != file_ofs){ + // data->out_file->seekp(file_ofs); + // } + + // Write chunk to disk + data->out_file->write(static_cast(pBuf), n); + + // Update and display progress + data->processed_bytes += n; + double percent = (double)data->processed_bytes / data->total_bytes * 100.0; + int current_percent = static_cast(percent); + + // Only print if the integer value has changed + if (current_percent > data->last_percentage){ + data->progress_callback(current_percent); + data->last_percentage = current_percent; + + // std::cout << "\rProgress: " << current_percent << "% (" + // << data->processed_bytes << "/" << data->total_bytes << " bytes)" << endl; + } - // resolves .. and . components and returns an absolute path without requiring the final path to exist. - fs::path target = fs::weakly_canonical(Filesystem::Path(base / entry_name)); + return n; +} - // cout << base << endl; - // cout << target << endl; +// ensure that entry_name is inside target_dir, to prevent path traversal attacks. +// assumes target_dir exists +bool is_safe(const std::string& target_dir, const std::string& entry_name){ + try { + // 1. Get absolute, normalized paths + // handles symlinks. and resolves .. and . components. throws error if path doesn't exist + Filesystem::Path base = fs::canonical(Filesystem::Path(target_dir)); + // confirms that base is a directory, and not a file + if (!fs::is_directory(base)) return false; + + // resolves .. and . components and returns an absolute path without requiring the final path to exist. + fs::path target = fs::weakly_canonical(Filesystem::Path(base / Filesystem::Path(entry_name))); - // 2. Use lexically_relative to find the path from base to target - fs::path rel = target.lexically_relative(base); + // cout << base << endl; + // cout << target << endl; - // 3. Validation: - // - If rel is empty, they are likely different roots - // - If rel starts with "..", it escaped the base - // - If rel is ".", it IS the base directory (usually safe) - if (rel.empty() || *rel.begin() == ".."){ - return false; - } + // 2. Use lexically_relative to find the path from base to target + fs::path rel = target.lexically_relative(base); - return true; - } catch (...){ - cout << "target_dir path doesn't exist." << endl; + // 3. Validation: + // - If rel is empty, they are likely different roots + // - If rel starts with "..", it escaped the base + // - If rel is ".", it IS the base directory (usually safe) + if (rel.empty() || *rel.begin() == ".."){ return false; } + + return true; + } catch (...){ + cout << "target_dir path doesn't exist." << endl; + return false; } +} - void unzip_file(const char* zip_path, const char* target_dir){ - Filesystem::Path p{zip_path}; - if (!fs::exists(p)){ - throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "unzip_all: Attempted to unzip a file that doesn't exist."); - } +void unzip_file( + const char* zip_path, + const char* target_dir, + std::function progress_callback, + std::function is_cancelled +){ + Filesystem::Path p{zip_path}; + if (!fs::exists(p)){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "unzip_file: Attempted to unzip a file that doesn't exist."); + } + + { + Filesystem::Path dir{target_dir}; + std::error_code ec{}; + fs::create_directories(dir, ec); + if (ec){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "unzip_file: Error creating the target directory " + std::string(target_dir) + ": " + ec.message()); + } + } - mz_zip_archive zip_archive; - memset(&zip_archive, 0, sizeof(zip_archive)); + mz_zip_archive zip_archive; + memset(&zip_archive, 0, sizeof(zip_archive)); - // Opens the ZIP file at zip_path - // zip_archive holds the state and metadata of the ZIP archive. - if (!mz_zip_reader_init_file(&zip_archive, zip_path, 0)){ - cout << "failed to run mz_zip_reader_init_file" << endl; - cout << "mz_zip_error: " << mz_zip_get_last_error(&zip_archive) << endl; - return; - } + // Opens the ZIP file at zip_path + // zip_archive holds the state and metadata of the ZIP archive. + if (!mz_zip_reader_init_file(&zip_archive, zip_path, 0)){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, + "unzip_file: failed to run mz_zip_reader_init_file. mz_zip_error: " + std::to_string(mz_zip_get_last_error(&zip_archive))); + } - // Get total number of files in the archive - int num_files = (int)mz_zip_reader_get_num_files(&zip_archive); + // This automatically calls mz_zip_reader_end when this function exits for any reason. + struct ZipCleanup { + mz_zip_archive* p; + ~ZipCleanup() { mz_zip_reader_end(p); } + } cleanup{&zip_archive}; - // calculate total uncompressed size - uint64_t total_uncompressed_size = 0; - for (int i = 0; i < num_files; i++){ - mz_zip_archive_file_stat file_stat; // holds info on the specific file + // Get total number of files in the archive + int num_files = (int)mz_zip_reader_get_num_files(&zip_archive); - // fills file_stat with the data for the current index - if (!mz_zip_reader_file_stat(&zip_archive, i, &file_stat)) continue; + // calculate total uncompressed size + uint64_t total_uncompressed_size = 0; + for (int i = 0; i < num_files; i++){ + if (is_cancelled()) throw OperationCancelledException(); - cout << std::to_string(file_stat.m_uncomp_size) << endl; - total_uncompressed_size += file_stat.m_uncomp_size; - } + mz_zip_archive_file_stat file_stat; // holds info on the specific file - uint64_t total_processed_bytes = 0; - for (int i = 0; i < num_files; i++){ - mz_zip_archive_file_stat file_stat; // holds info on the specific file + // fills file_stat with the data for the current index + if (!mz_zip_reader_file_stat(&zip_archive, i, &file_stat)) continue; - // fills file_stat with the data for the current index - if (!mz_zip_reader_file_stat(&zip_archive, i, &file_stat)) continue; + // cout << std::to_string(file_stat.m_uncomp_size) << endl; + total_uncompressed_size += file_stat.m_uncomp_size; + } - // Checks if the current entry is a folder. Miniz treats folders as entries; - // this code skips them to avoid trying to "write" a folder as if it were a file. - if (mz_zip_reader_is_file_a_directory(&zip_archive, i)){ - continue; - } + uint64_t total_processed_bytes = 0; + for (int i = 0; i < num_files; i++){ + if (is_cancelled()) throw OperationCancelledException(); - // Construct your output path (e.g., target_dir + file_stat.m_filename) - std::string out_path = std::string(target_dir) + "/" + file_stat.m_filename; - Filesystem::Path const parent_dir{Filesystem::Path(out_path).parent_path()}; + mz_zip_archive_file_stat file_stat; // holds info on the specific file - // Create the entire directory, including intermediate directories for this file - std::error_code ec{}; - fs::create_directories(parent_dir, ec); - if (ec){ - std::cerr << "Error creating " << parent_dir << ": " << ec.message() << std::endl; - ec.clear(); - } + // fills file_stat with the data for the current index + if (!mz_zip_reader_file_stat(&zip_archive, i, &file_stat)) continue; + + // Checks if the current entry is a folder. Miniz treats folders as entries; + // this code skips them to avoid trying to "write" a folder as if it were a file. + if (mz_zip_reader_is_file_a_directory(&zip_archive, i)){ + continue; + } + + // ensure that entry_name is inside target_dir. to prevent path traversal attacks. + if (!is_safe(target_dir, file_stat.m_filename)){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "unzip_file: Attempted to unzip a file that was trying to leave its base directory. This is a security risk."); + } + + // Construct your output path (e.g., target_dir + file_stat.m_filename) + Filesystem::Path out_path = Filesystem::Path(target_dir) / Filesystem::Path(file_stat.m_filename); + Filesystem::Path const parent_dir{out_path.parent_path()}; + + // Create the entire directory, including intermediate directories for this file + std::error_code ec{}; + fs::create_directories(parent_dir, ec); + if (ec){ + std::cerr << "Error creating " << parent_dir << ": " << ec.message() << std::endl; + ec.clear(); + } - // ensure that entry_name is inside target_dir. to prevent path traversal attacks. - if (!is_safe(target_dir, file_stat.m_filename)){ - throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "unzip_all: Attempted to unzip a file that was trying to leave its base directory. This is a security risk."); + + std::ofstream out_file(out_path.string(), std::ios::binary); // std::ios::binary is to prevent line-ending conversions. + ProgressData progress = { &out_file, total_uncompressed_size, total_processed_bytes, -1, progress_callback, is_cancelled }; + + // Extract using the callback + // decompresses the file in chunks and repeatedly calls write_callback to save those chunks to the disk via the out_file + mz_bool status = mz_zip_reader_extract_to_callback(&zip_archive, i, write_callback, &progress, 0); + + if (!status){ + out_file.close(); + if (is_cancelled()){ + // close and delete the partially unzipped file + fs::remove(out_path, ec); + throw OperationCancelledException(); } - - std::ofstream out_file(out_path, std::ios::binary); // std::ios::binary is to prevent line-ending conversions. - ProgressData progress = { &out_file, total_uncompressed_size, total_processed_bytes, -1 }; - - // Extract using the callback - // decompresses the file in chunks and repeatedly calls write_callback to save those chunks to the disk via the out_file - mz_zip_reader_extract_to_callback(&zip_archive, i, write_callback, &progress, 0); - std::cout << "\nFinished: " << file_stat.m_filename << std::endl; - total_processed_bytes += file_stat.m_uncomp_size; } - mz_zip_reader_end(&zip_archive); + // std::cout << "\nFinished: " << file_stat.m_filename << std::endl; + total_processed_bytes += file_stat.m_uncomp_size; } - // void unzip_file(const std::string& zip_path, const std::string& output_dir){ - // cout << "try to unzip the file." << endl; - // miniz_cpp::zip_file archive(zip_path); - - // // create folder structure before extracting. - // // since miniz-cpp does not automatically create subdirectories if they exist within the zip archive - // std::vector const info_list = archive.infolist(); - // auto const current_directory = std::filesystem::current_path(); - // std::error_code ec{}; - // for(miniz_cpp::zip_info const & info: info_list ){ - // std::filesystem::path const p{(std::filesystem::path(output_dir) / info.filename).parent_path()}; - // // Create the entire directory tree for this file - // std::filesystem::create_directories(p, ec); - - // if (ec){ - // std::cerr << "Error creating " << p << ": " << ec.message() << std::endl; - // ec.clear(); - // } - // } +} + +// void unzip_file(const std::string& zip_path, const std::string& output_dir){ +// cout << "try to unzip the file." << endl; +// miniz_cpp::zip_file archive(zip_path); + +// // create folder structure before extracting. +// // since miniz-cpp does not automatically create subdirectories if they exist within the zip archive +// std::vector const info_list = archive.infolist(); +// auto const current_directory = std::filesystem::current_path(); +// std::error_code ec{}; +// for(miniz_cpp::zip_info const & info: info_list ){ +// std::filesystem::path const p{(std::filesystem::path(output_dir) / info.filename).parent_path()}; +// // Create the entire directory tree for this file +// std::filesystem::create_directories(p, ec); + +// if (ec){ +// std::cerr << "Error creating " << p << ": " << ec.message() << std::endl; +// ec.clear(); +// } +// } - // // Extract all files to the specified path - // archive.extractall(output_dir); +// // Extract all files to the specified path +// archive.extractall(output_dir); - // cout << "done unzipping the file." << endl; +// cout << "done unzipping the file." << endl; - // } +// } } diff --git a/SerialPrograms/Source/CommonFramework/Tools/FileUnzip.h b/SerialPrograms/Source/CommonFramework/Tools/FileUnzip.h index a2b509bdba..9eb26fd931 100644 --- a/SerialPrograms/Source/CommonFramework/Tools/FileUnzip.h +++ b/SerialPrograms/Source/CommonFramework/Tools/FileUnzip.h @@ -8,10 +8,20 @@ #define PokemonAutomation_FileUnzip_H #include +#include namespace PokemonAutomation{ -void unzip_file(const char* zip_path, const char* target_dir); +// unzips the zip file located in zip_path, to target_dir +// if target_dir doesn't already exist, it will create it. +// throw OperationCancelledException if the is_cancelled callback returns true +// throw InternalProgramError if unzipping fails. +void unzip_file( + const char* zip_path, + const char* target_dir, + std::function progress_callback, + std::function is_cancelled +); } #endif diff --git a/SerialPrograms/Source/StaticRegistrationQt.cpp b/SerialPrograms/Source/StaticRegistrationQt.cpp index 47d1fa0d9e..f4cabb80a6 100644 --- a/SerialPrograms/Source/StaticRegistrationQt.cpp +++ b/SerialPrograms/Source/StaticRegistrationQt.cpp @@ -36,6 +36,9 @@ #include "CommonFramework/Options/QtWidget/LabelCellWidget.h" #include "CommonFramework/Notifications/EventNotificationWidget.h" +// Resource Download +#include "CommonFramework/ResourceDownload/ResourceDownloadWidget.h" + // Integrations #include "Integrations/DiscordIntegrationSettingsWidget.h" @@ -99,6 +102,12 @@ void register_all_statics(){ RegisterConfigWidget(); RegisterConfigWidget(); + // Resource Download + RegisterConfigWidget(); + RegisterConfigWidget(); + RegisterConfigWidget(); + RegisterConfigWidget(); + // Integrations RegisterConfigWidget(); diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index f0a89d4520..c33ca65cd2 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -471,7 +471,7 @@ file(GLOB LIBRARY_SOURCES Source/CommonFramework/PersistentSettings.cpp Source/CommonFramework/PersistentSettings.h Source/CommonFramework/ProgramSession.cpp - Source/CommonFramework/ProgramSession.h + Source/CommonFramework/ProgramSession.h Source/CommonFramework/ProgramStats/StatsDatabase.cpp Source/CommonFramework/ProgramStats/StatsDatabase.h Source/CommonFramework/ProgramStats/StatsTracking.cpp @@ -487,6 +487,16 @@ file(GLOB LIBRARY_SOURCES Source/CommonFramework/Recording/StreamHistoryTracker_SaveFrames.h Source/CommonFramework/Recording/StreamRecorder.cpp Source/CommonFramework/Recording/StreamRecorder.h + Source/CommonFramework/ResourceDownload/ResourceDownloadHelpers.cpp + Source/CommonFramework/ResourceDownload/ResourceDownloadHelpers.h + Source/CommonFramework/ResourceDownload/ResourceDownloadOptions.cpp + Source/CommonFramework/ResourceDownload/ResourceDownloadOptions.h + Source/CommonFramework/ResourceDownload/ResourceDownloadRow.cpp + Source/CommonFramework/ResourceDownload/ResourceDownloadRow.h + Source/CommonFramework/ResourceDownload/ResourceDownloadTable.cpp + Source/CommonFramework/ResourceDownload/ResourceDownloadTable.h + Source/CommonFramework/ResourceDownload/ResourceDownloadWidget.cpp + Source/CommonFramework/ResourceDownload/ResourceDownloadWidget.h Source/CommonFramework/Startup/NewVersionCheck.cpp Source/CommonFramework/Startup/NewVersionCheck.h Source/CommonFramework/Startup/SetupSettings.cpp @@ -497,6 +507,8 @@ file(GLOB LIBRARY_SOURCES Source/CommonFramework/Tools/ErrorDumper.h Source/CommonFramework/Tools/FileDownloader.cpp Source/CommonFramework/Tools/FileDownloader.h + Source/CommonFramework/Tools/FileHash.cpp + Source/CommonFramework/Tools/FileHash.h Source/CommonFramework/Tools/FileUnzip.cpp Source/CommonFramework/Tools/FileUnzip.h Source/CommonFramework/Tools/GlobalThreadPools.cpp