diff --git a/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp b/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp index 0b42b0cc46..55c22802d2 100644 --- a/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp +++ b/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.cpp @@ -260,6 +260,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 afc266fce0..7f138db51d 100644 --- a/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.h +++ b/SerialPrograms/Source/CommonFramework/GlobalSettingsPanel.h @@ -17,6 +17,7 @@ #include "Common/Cpp/Options/ButtonOption.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/ResourceDownload/ResourceDownloadHelpers.h b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadHelpers.h index f0305bde6b..2ce9fbc63f 100644 --- a/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadHelpers.h +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadHelpers.h @@ -22,8 +22,8 @@ namespace Filesystem{ struct DownloadedResourceMetadata{ std::string resource_name; std::optional version_num; - size_t size_compressed_bytes; - size_t size_decompressed_bytes; + uint64_t size_compressed_bytes; + uint64_t size_decompressed_bytes; std::string url; std::string sha256; }; 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..26776eeb76 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadOptions.h @@ -0,0 +1,87 @@ +/* Resource Download Row + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_ResourceDownloadOptions_H +#define PokemonAutomation_ResourceDownloadOptions_H + +// #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 ConfigOptionImpl{ +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..09e94cb359 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadRow.cpp @@ -0,0 +1,644 @@ +/* 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/ListenerSet.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; + + + +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// DownloadThread +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +DownloadThread::~DownloadThread(){ + // cout << "~DownloadThread" << endl; + this->cancel(); + m_worker.wait_and_ignore_exceptions(); +} +DownloadThread::DownloadThread(ResourceDownloadRow& row, Mutex& lock, ConditionVariable& cv) + : m_row(row) + , m_download_lock(lock) + , m_download_cv(cv) +{} + +void DownloadThread::start_download_thread(){ + m_worker = GlobalThreadPools::unlimited_normal().dispatch_now_blocking( + [this]{ + + { + std::unique_lock lock(m_download_lock); + m_download_cv.wait(lock, [this] { return m_row.is_download_ready_to_start() || m_stopping; }); + + if (m_stopping) return; + } + + // runs when lambda is finished + // updates action state, removes self from download queue + struct ScopeGuard { + DownloadThread* thread_ptr; + ~ScopeGuard() { + thread_ptr->m_stopping = true; + thread_ptr->m_row.on_download_finished(); + } + } guard{this}; + + try { + // std::this_thread::sleep_for(std::chrono::seconds(7)); + RemoteMetadata& remote_handle = m_row.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; + + m_row.update_table_label(true); + + }catch(OperationCancelledException&){ + // user cancelled action + + m_row.update_table_label(false); + + }catch(OperationFailedException&){ + m_row.update_table_label(false); + + m_row.report_download_failed(); + }catch(...){ + m_row.update_table_label(false); + + m_row.report_exception_caught("ResourceDownloadButton::start_download"); + } + + } + ); + +} + +void DownloadThread::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; + uint64_t 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( + *this, + logger, + url, + zip_path, + expected_size, + [this](uint64_t bytes_done, uint64_t total_bytes){ + m_row.report_download_progress(bytes_done, total_bytes); + } + ); + + // hash + std::string hash = + hash_file( + *this, + zip_path, + [this](uint64_t bytes_done, uint64_t total_bytes){ + m_row.report_hash_progress(bytes_done, total_bytes); + } + ); + std::string expected_hash = resource_metadata.sha256; + 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( + *this, + zip_path.c_str(), + resource_directory.c_str(), + [this](uint64_t bytes_done, uint64_t total_bytes){ + m_row.report_unzip_progress(bytes_done, total_bytes); + } + ); + + // delete old zip file + fs::remove(Filesystem::Path(zip_path)); + + throw_if_cancelled(); + + }catch(OperationCancelledException&){ + // delete directory and the resource + fs::remove_all(Filesystem::Path(resource_directory)); + + throw; + }catch(OperationFailedException&){ + // delete directory and the resource + fs::remove_all(Filesystem::Path(resource_directory)); + + throw; + }catch(...){ + // delete directory and the resource + fs::remove_all(Filesystem::Path(resource_directory)); + + throw; + } + +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// 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::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)) + {} + + ListenerSet listeners; + + 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; + + +}; + +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::update_table_label(bool success){ + set_is_downloaded(success); + set_version_status(success ? ResourceVersionStatus::CURRENT : ResourceVersionStatus::NOT_APPLICABLE); +} + + + +ResourceDownloadRow::~ResourceDownloadRow(){ + // cout << "~ResourceDownloadRow" << endl; + m_pre_download_thread.wait_and_ignore_exceptions(); + m_delete_thread.wait_and_ignore_exceptions(); +} +ResourceDownloadRow::ResourceDownloadRow( + ResourceDownloadTable& parent_table, + Mutex& lock, + ConditionVariable& cv, + uint16_t index, + DownloadedResourceMetadata local_metadata, + bool is_downloaded, + std::optional version_num, + ResourceVersionStatus version_status +) + : StaticTableRow(local_metadata.resource_name) + , m_parent_table(parent_table) + , m_download_lock(lock) + , m_download_cv(cv) + , m_action_state(ActionState::READY) + , m_index(index) + , 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_pre_download_thread = GlobalThreadPools::unlimited_normal().dispatch_now_blocking( + [this]{ + try { + if (!is_given_action_state(ActionState::PRE_DOWNLOAD)){ + return; + } + + // 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_action_state(ActionState::READY); + report_metadata_fetch_finished(predownload_warning); + + }catch(OperationFailedException&){ + // cout << "failed" << endl; + // update_table_label(false); + update_action_state(ActionState::READY); + report_download_failed(); + return; + }catch(...){ + // update_table_label(false); + update_action_state(ActionState::READY); + // cout << "Exception thrown in thread" << endl; + report_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(){ + if (!is_given_action_state(ActionState::PRE_DOWNLOAD)){ + return; + } + + update_action_state(ActionState::DOWNLOADING); + + cancel_download_thread(); // cancels old download thread + + m_parent_table.add_row_to_download_list(m_index); + m_download_thread = std::make_unique(*this, m_download_lock, m_download_cv); + m_download_thread->start_download_thread(); +} + + +void ResourceDownloadRow::start_delete(){ + m_delete_thread = GlobalThreadPools::unlimited_normal().dispatch_now_blocking( + [this]{ + try { + if (!is_given_action_state(ActionState::PRE_DELETE)){ + return; + } + update_action_state(ActionState::DELETING); + + 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_action_state(ActionState::READY); + }catch(...){ + update_action_state(ActionState::READY); + report_exception_caught("ResourceDownloadButton::start_delete"); + return; + } + } + ); + +} + +void ResourceDownloadRow::on_download_finished(){ + + update_action_state(ActionState::READY); + remove_self_from_download_queue(); +} + +void ResourceDownloadRow::update_action_state(ActionState state){ + std::lock_guard lock(m_action_state_lock); + { + switch (state){ + case ActionState::PRE_DOWNLOAD: + // action state can only enter the PRE_DOWNLOAD state + // if going from the READY state + if (m_action_state == ActionState::READY){ + m_download_button.set_enabled(false); + m_delete_button.set_enabled(false); + m_cancel_button.set_enabled(true); + m_action_state = state; + cout << "ActionState::PRE_DOWNLOAD" << endl; + } + break; + case ActionState::DOWNLOADING: + if (m_action_state == ActionState::PRE_DOWNLOAD || m_action_state == ActionState::PRE_CANCEL){ + m_download_button.set_enabled(false); + m_delete_button.set_enabled(false); + m_cancel_button.set_enabled(true); + m_action_state = state; + cout << "ActionState::DOWNLOADING" << endl; + } + break; + case ActionState::PRE_DELETE: + // action state can only enter the PRE_DELETE state + // if going from the READY state + if (m_action_state == ActionState::READY){ + m_download_button.set_enabled(false); + m_delete_button.set_enabled(false); + m_cancel_button.set_enabled(false); + m_action_state = state; + cout << "ActionState::PRE_DELETE" << endl; + } + break; + case ActionState::DELETING: + // action state can only enter the DELETING state + // if going from the PRE_DELETE state + if (m_action_state == ActionState::PRE_DELETE){ + m_download_button.set_enabled(false); + m_delete_button.set_enabled(false); + m_cancel_button.set_enabled(false); + m_action_state = state; + cout << "ActionState::DELETING" << endl; + } + break; + case ActionState::PRE_CANCEL: + // action state can only enter the PRE_CANCEL state + // if going from the DOWNLOADING state + if (m_action_state == ActionState::DOWNLOADING){ + m_download_button.set_enabled(false); + m_delete_button.set_enabled(false); + m_cancel_button.set_enabled(false); + m_action_state = state; + cout << "ActionState::PRE_CANCEL" << endl; + } + break; + case ActionState::CANCELLING: + // action state can only enter the CANCELLING state + // if going from the PRE_CANCEL state + if (m_action_state == ActionState::PRE_CANCEL){ + m_download_button.set_enabled(false); + m_delete_button.set_enabled(false); + m_cancel_button.set_enabled(false); + m_action_state = state; + cout << "ActionState::CANCELLING" << endl; + } + break; + case ActionState::READY: + m_download_button.set_enabled(true); + m_delete_button.set_enabled(true); + m_cancel_button.set_enabled(true); + m_action_state = state; + cout << "ActionState::READY" << endl; + break; + default: + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "update_action_state: Unknown enum."); + } + } + + report_action_state_updated(); +} + +ActionState ResourceDownloadRow::get_action_state(){ + std::lock_guard lock(m_action_state_lock); + return m_action_state; +} + +void ResourceDownloadRow::add_listener(DownloadListener& listener){ + auto scope = m_lifetime_sanitizer.check_scope(); + m_data->listeners.add(listener); +} +void ResourceDownloadRow::remove_listener(DownloadListener& listener){ + auto scope = m_lifetime_sanitizer.check_scope(); + m_data->listeners.remove(listener); +} + +void ResourceDownloadRow::report_download_progress(uint64_t bytes_done, uint64_t total_bytes){ + auto scope = m_lifetime_sanitizer.check_scope(); + m_data->listeners.run_method(&DownloadListener::on_download_progress, bytes_done, total_bytes); +} +void ResourceDownloadRow::report_unzip_progress(uint64_t bytes_done, uint64_t total_bytes){ + auto scope = m_lifetime_sanitizer.check_scope(); + m_data->listeners.run_method(&DownloadListener::on_unzip_progress, bytes_done, total_bytes); +} +void ResourceDownloadRow::report_hash_progress(uint64_t bytes_done, uint64_t total_bytes){ + auto scope = m_lifetime_sanitizer.check_scope(); + m_data->listeners.run_method(&DownloadListener::on_hash_progress, bytes_done, total_bytes); +} + +void ResourceDownloadRow::report_metadata_fetch_finished(const std::string& popup_message){ + auto scope = m_lifetime_sanitizer.check_scope(); + m_data->listeners.run_method(&DownloadListener::on_metadata_fetch_finished, popup_message); +} +void ResourceDownloadRow::report_exception_caught(const std::string& function_name){ + auto scope = m_lifetime_sanitizer.check_scope(); + m_data->listeners.run_method(&DownloadListener::on_exception_caught, function_name); +} +void ResourceDownloadRow::report_download_failed(){ + auto scope = m_lifetime_sanitizer.check_scope(); + m_data->listeners.run_method(&DownloadListener::on_download_failed); +} + +void ResourceDownloadRow::report_action_state_updated(){ + auto scope = m_lifetime_sanitizer.check_scope(); + m_data->listeners.run_method(&DownloadListener::on_action_state_updated); +} + + +bool ResourceDownloadRow::is_download_ready_to_start(){ + return m_parent_table.is_download_ready_to_start(m_index); +} + +void ResourceDownloadRow::remove_self_from_download_queue(){ + m_parent_table.remove_row_from_download_list(m_index); +} + +bool ResourceDownloadRow::is_given_action_state(ActionState state){ + std::lock_guard lock(m_action_state_lock); + return m_action_state == state; +} + +void ResourceDownloadRow::cancel_download_thread(){ + if (m_download_thread){ // if thread already exists + m_download_thread->cancel(); // stop the thread + } +} + +} diff --git a/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadRow.h b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadRow.h new file mode 100644 index 0000000000..0ce5eeaa15 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadRow.h @@ -0,0 +1,165 @@ +/* Resource Download Row + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_ResourceDownloadRow_H +#define PokemonAutomation_ResourceDownloadRow_H + +#include "Common/Cpp/Containers/Pimpl.h" +#include "Common/Cpp/Concurrency/AsyncTask.h" +// #include "Common/Cpp/Concurrency/ConditionVariable.h" +#include "Common/Cpp/CancellableScope.h" +#include "Common/Cpp/LifetimeSanitizer.h" +// #include "CommonFramework/Tools/GlobalThreadPools.h" +#include "Common/Cpp/Options/StaticTableOption.h" +#include "ResourceDownloadHelpers.h" +#include "ResourceDownloadOptions.h" +#include "ResourceDownloadTable.h" +// #include + +namespace PokemonAutomation{ + + +class DownloadThread : public CancellableScope{ + +public: + ~DownloadThread(); + DownloadThread(ResourceDownloadRow& row, Mutex& lock, ConditionVariable& cv); + +public: + void start_download_thread(); + + // throws OperationCancelledException if the user cancels the action + void run_download(DownloadedResourceMetadata resource_metadata); + + + +private: + ResourceDownloadRow& m_row; + AsyncTask m_worker; + + std::atomic m_stopping{false}; + Mutex& m_download_lock; + ConditionVariable& m_download_cv; +}; + +enum class ActionState{ + PRE_DOWNLOAD, + DOWNLOADING, + PRE_DELETE, + DELETING, + PRE_CANCEL, + CANCELLING, + READY, +}; +class ResourceDownloadRow : public StaticTableRow{ +public: + ~ResourceDownloadRow(); + ResourceDownloadRow( + ResourceDownloadTable& parent_table, + Mutex& lock, + ConditionVariable& cv, + uint16_t index, + DownloadedResourceMetadata local_metadata, + bool is_downloaded, + std::optional version_num, + ResourceVersionStatus version_status + ); + +public: + struct DownloadListener{ + virtual void on_download_progress(uint64_t bytes_done, uint64_t total_bytes){} + virtual void on_unzip_progress(uint64_t bytes_done, uint64_t total_bytes){} + virtual void on_hash_progress(uint64_t bytes_done, uint64_t total_bytes){} + + virtual void on_metadata_fetch_finished(const std::string& popup_message){} + virtual void on_exception_caught(const std::string& function_name){} + virtual void on_download_failed(){} + + virtual void on_action_state_updated(){} + }; + + void add_listener(DownloadListener& listener); + void remove_listener(DownloadListener& listener); + + void report_download_progress(uint64_t bytes_done, uint64_t total_bytes); + void report_unzip_progress(uint64_t bytes_done, uint64_t total_bytes); + void report_hash_progress(uint64_t bytes_done, uint64_t total_bytes); + + void report_metadata_fetch_finished(const std::string& popup_message); + void report_exception_caught(const std::string& function_name); + void report_download_failed(); + + void report_action_state_updated(); + + +public: + void set_version_status(ResourceVersionStatus version_status); + void set_is_downloaded(bool is_downloaded); + void update_table_label(bool success); + + 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(); + + void on_download_finished(); + + void start_delete(); + + // READY: can come from any state + // PRE_DOWNLOAD, PRE_DELETE, PRE_CANCEL: can only come from READY + // DELETING, CANCELLING: can only come from their respective PRE state + // DOWNLOADING: can come from either PRE_DOWNLOAD or PRE_CANCEL + void update_action_state(ActionState state); + + ActionState get_action_state(); + + bool is_download_ready_to_start(); + void remove_self_from_download_queue(); + + bool is_given_action_state(ActionState state); + + void cancel_download_thread(); + +private: + std::once_flag init_flag; + std::unique_ptr m_remote_metadata; + + ResourceDownloadTable& m_parent_table; + Mutex& m_download_lock; + ConditionVariable& m_download_cv; + + ActionState m_action_state; + uint16_t m_index; + 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_pre_download_thread; + AsyncTask m_delete_thread; + + std::unique_ptr m_download_thread; + + Mutex m_action_state_lock; + + LifetimeSanitizer m_lifetime_sanitizer; + + + + +}; + +} +#endif diff --git a/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadTable.cpp b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadTable.cpp new file mode 100644 index 0000000000..fc13f4ca42 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadTable.cpp @@ -0,0 +1,157 @@ +/* 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> ResourceDownloadTable::get_resource_download_rows(){ + std::vector> resource_rows; + std::vector resource_list; + try{ + resource_list = local_resource_download_list(); + }catch(FileException&){ + return {}; + } + + for (uint16_t index = 0; index < resource_list.size(); index++){ + DownloadedResourceMetadata resource = resource_list[index]; + 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(*this, m_lock, m_cv, index, 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_row_to_download_list(uint16_t row_index){ + cout << "add_row_to_download_list" << endl; + std::lock_guard lg(m_lock); + m_download_queue.push_back(row_index); + +} + +void ResourceDownloadTable::remove_row_from_download_list(uint16_t row_index){ + std::lock_guard lg(m_lock); + + // this requires C++20 + std::erase(m_download_queue, row_index); + m_cv.notify_all(); +} + +void ResourceDownloadTable::add_resource_download_rows(){ + for (std::unique_ptr& row_ptr : m_resource_rows){ + add_row(row_ptr.get()); + } +} + +bool ResourceDownloadTable::is_download_ready_to_start(uint16_t row_index){ + + uint16_t MAX_CONCURRENT_DOWNLOADS = 10; + + // std::lock_guard lg(m_lock); + auto it = std::find(m_download_queue.begin(), m_download_queue.end(), row_index); + if (it == m_download_queue.end()){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "is_download_ready_to_start: row_index not found within m_download_queue."); + } + + uint16_t download_position = std::distance(m_download_queue.begin(), it); + + // cout << "download_position: " << std::to_string(download_position) << endl; + + return download_position < MAX_CONCURRENT_DOWNLOADS; + +} + + + +// 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..2669fe11fa --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadTable.h @@ -0,0 +1,56 @@ +/* Resource Download Table + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_ResourceDownloadTable_H +#define PokemonAutomation_ResourceDownloadTable_H + +#include +#include "Common/Cpp/Concurrency/AsyncTask.h" +#include "Common/Cpp/Concurrency/Mutex.h" +#include "Common/Cpp/Concurrency/ConditionVariable.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; + + void add_row_to_download_list(uint16_t row_index); + void remove_row_from_download_list(uint16_t row_index); + + // return true if given row_index's position in m_download_queue is less than MAX_CONCURRENT_DOWNLOADS + // ASSUMES: the calling thread holds the m_lock. therefore, this function doesn't lock the mutex when accessing m_download_queue. + bool is_download_ready_to_start(uint16_t row_index); + +private: + std::vector> get_resource_download_rows(); + 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; + std::deque m_download_queue; + + Mutex m_lock; + ConditionVariable m_cv; + + // 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..67d311b372 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadWidget.cpp @@ -0,0 +1,535 @@ +/* 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.")); + +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// DownloadButtonWidget +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +template class RegisterConfigWidget; +DownloadButtonWidget::~DownloadButtonWidget(){ + // cout << "Destructor for DownloadButtonWidget" << endl; + // m_value.disconnect(this); + m_row.remove_listener(*this); +} +DownloadButtonWidget::DownloadButtonWidget(QWidget& parent, ResourceDownloadButton& value) + : QWidget(&parent) + , ConfigWidget(value, *this) + , m_value(value) + , m_row(value.row) +{ + + QHBoxLayout* layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + + m_button = new QPushButton("Download", this); + // m_widget = this; + + layout->addWidget(m_button); + + // cout << "Constructor for DownloadButtonWidget" << endl; + + QFont font; + font.setBold(true); + m_button->setFont(font); + m_button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + 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_action_state(), which updates the button state + // also, fetch json + connect( + m_button, &QPushButton::clicked, + this, [this](){ + if (!m_row.is_given_action_state(ActionState::READY)){ + return; + } + m_row.update_action_state(ActionState::PRE_DOWNLOAD); + m_row.ensure_remote_metadata_loaded(); + } + ); + + + m_row.add_listener(*this); +} + + +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_row.is_given_action_state(ActionState::PRE_DOWNLOAD) + || m_row.is_given_action_state(ActionState::DOWNLOADING)) + { + 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_row.start_download(); + return; + } + if (clicked == cancel){ + m_row.update_action_state(ActionState::READY); + return; + } +} + +// 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 +void DownloadButtonWidget::on_metadata_fetch_finished(const std::string& popup_message){ + QMetaObject::invokeMethod(this, [this, popup_message]{ + show_download_confirm_box("Download", popup_message); + }, Qt::QueuedConnection); + +} + +// if the thread catches an exception, show an error box +// since exceptions can't bubble up as usual +// handles all exception_caught() reported by ResourceDownloadRow +void DownloadButtonWidget::on_exception_caught(const std::string& function_name){ + QMetaObject::invokeMethod(this, [function_name]{ + show_error_box(function_name); + }, Qt::QueuedConnection); + +} + +void DownloadButtonWidget::on_download_failed(){ + QMetaObject::invokeMethod(this, []{ + show_download_failed_box(); + }, Qt::QueuedConnection); +} + +void DownloadButtonWidget::on_action_state_updated(){ + QMetaObject::invokeMethod(this, [this]{ + update_UI_state(); + }, Qt::QueuedConnection); +} + + +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// DeleteButtonWidget +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +template class RegisterConfigWidget; +DeleteButtonWidget::~DeleteButtonWidget(){ + m_row.remove_listener(*this); +} +DeleteButtonWidget::DeleteButtonWidget(QWidget& parent, ResourceDeleteButton& value) + : QWidget(&parent) + , ConfigWidget(value, *this) + , m_value(value) + , m_row(value.row) +{ + QHBoxLayout* layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + + m_button = new QPushButton("Delete", this); + // m_widget = m_button; + + layout->addWidget(m_button); + + QFont font; + font.setBold(true); + m_button->setFont(font); + m_button->setText("Delete"); + m_button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + 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_action_state(), which updates the button state + // also, show the delete confirm box + connect( + m_button, &QPushButton::clicked, + this, [&](bool){ + if (!m_row.is_given_action_state(ActionState::READY)){ + return; + } + m_row.update_action_state(ActionState::PRE_DELETE); + show_delete_confirm_box(); + // cout << "Clicked Delete Button" << endl; + } + ); + + m_row.add_listener(*this); +} + + +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_row.is_given_action_state(ActionState::PRE_DELETE) + || m_row.is_given_action_state(ActionState::DELETING) + ){ + 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_row.start_delete(); + return; + } + if (clicked == cancel){ + m_row.update_action_state(ActionState::READY); + return; + } +} + +// when action_state_updated, update the UI state to match +void DeleteButtonWidget::on_action_state_updated(){ + QMetaObject::invokeMethod(this, [this]{ + update_UI_state(); + }, Qt::QueuedConnection); + +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// CancelButtonWidget +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +template class RegisterConfigWidget; +CancelButtonWidget::~CancelButtonWidget(){ + m_row.remove_listener(*this); +} +CancelButtonWidget::CancelButtonWidget(QWidget& parent, ResourceCancelButton& value) + : QWidget(&parent) + , ConfigWidget(value, *this) + , m_value(value) + , m_row(value.row) +{ + QHBoxLayout* layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + + m_button = new QPushButton("Cancel", this); + // m_widget = m_button; + + layout->addWidget(m_button); + + QFont font; + font.setBold(true); + m_button->setFont(font); + m_button->setText("Cancel"); + m_button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + 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_action_state(), which updates the button state + // also, set cancel state to true + connect( + m_button, &QPushButton::clicked, + this, [&](bool){ + if (!m_row.is_given_action_state(ActionState::DOWNLOADING)){ + return; + } + m_row.update_action_state(ActionState::PRE_CANCEL); + show_cancel_confirm_box(); + cout << "Clicked Cancel Button" << endl; + } + ); + + m_row.add_listener(*this); + +} + +void CancelButtonWidget::show_cancel_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 = "Cancel Download"; + std::string message_body = "Are you sure you want to cancel this download?"; + + 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 Cancel" << endl; + + if (!m_row.is_given_action_state(ActionState::PRE_CANCEL)){ + // if the download finishes and goes back to READY state before the user clicks Yes to cancel + // then nothing happens + return; + } + + m_row.update_action_state(ActionState::CANCELLING); + + m_row.cancel_download_thread(); + + return; + } + if (clicked == cancel){ + if (!m_row.is_given_action_state(ActionState::PRE_CANCEL)){ + return; + } + + m_row.update_action_state(ActionState::DOWNLOADING); + return; + } +} + + +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_row.is_given_action_state(ActionState::PRE_CANCEL) + || m_row.is_given_action_state(ActionState::CANCELLING) + ){ + m_button->setText("Cancelling..."); + } + } +} + +// when action_state_updated, update the UI state to match +void CancelButtonWidget::on_action_state_updated(){ + QMetaObject::invokeMethod(this, [this]{ + update_UI_state(); + }, Qt::QueuedConnection); +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////// +// ProgressBarWidget +///////////////////////////////////////////////////////////////////////////////////////////////////////// + +template class RegisterConfigWidget; +ProgressBarWidget::~ProgressBarWidget(){ + // cout << "Destructor for ProgressBarWidget" << endl; + m_row.remove_listener(*this); +} +ProgressBarWidget::ProgressBarWidget(QWidget& parent, ResourceProgressBar& value) + : QWidget(&parent) + , ConfigWidget(value, *this) + , m_value(value) + , m_row(value.row) +{ + + // 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); + + m_row.add_listener(*this); +} + + +void ProgressBarWidget::update_UI_state(){ + ActionState state = m_row.get_action_state(); + switch (state){ + case ActionState::PRE_DOWNLOAD: + case ActionState::DOWNLOADING: + m_status_label->setText("Downloading"); + if (m_progress_bar->isHidden()) { + m_progress_bar->show(); + } + break; + case ActionState::PRE_DELETE: + case ActionState::DELETING: + // m_status_label->setText(""); + // m_progress_bar->hide(); + m_progress_bar->setValue(0); + break; + case ActionState::PRE_CANCEL: + case ActionState::CANCELLING: + // m_status_label->setText(""); + // m_progress_bar->hide(); + m_progress_bar->setValue(0); + break; + case ActionState::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."); + } +} + +void ProgressBarWidget::update_progress_bar(int percentage, const std::string& text){ + if (m_progress_bar->isHidden()) { + m_progress_bar->show(); // Make it visible when progress starts + } + m_status_label->setText(QString::fromStdString(text)); + m_progress_bar->setValue(percentage); +} + +void ProgressBarWidget::update_progress_bar(uint64_t bytes_done, uint64_t total_bytes, const std::string& text){ + double percent = total_bytes > 0 ? (static_cast(bytes_done) / total_bytes) * 100.0 : 0; + int current_percent = static_cast(percent); + int last_percentage = m_progress_bar->value(); + // Only update UI if integer value has changed + if (current_percent != last_percentage){ + update_progress_bar(current_percent, text); + } +} + +void ProgressBarWidget::on_download_progress(uint64_t bytes_done, uint64_t total_bytes){ + QMetaObject::invokeMethod(this, [this, bytes_done, total_bytes]{ + update_progress_bar(bytes_done, total_bytes, "Downloading"); + }, Qt::QueuedConnection); + +} +void ProgressBarWidget::on_unzip_progress(uint64_t bytes_done, uint64_t total_bytes){ + QMetaObject::invokeMethod(this, [this, bytes_done, total_bytes]{ + update_progress_bar(bytes_done, total_bytes, "Unzipping"); + }, Qt::QueuedConnection); +} +void ProgressBarWidget::on_hash_progress(uint64_t bytes_done, uint64_t total_bytes){ + QMetaObject::invokeMethod(this, [this, bytes_done, total_bytes]{ + update_progress_bar(bytes_done, total_bytes, "Verifying"); + }, Qt::QueuedConnection); +} +// when action_state_updated, update the UI state to match +void ProgressBarWidget::on_action_state_updated(){ + QMetaObject::invokeMethod(this, [this]{ + update_UI_state(); + }, Qt::QueuedConnection); + +} + +} diff --git a/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadWidget.h b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadWidget.h new file mode 100644 index 0000000000..0e1274b1f5 --- /dev/null +++ b/SerialPrograms/Source/CommonFramework/ResourceDownload/ResourceDownloadWidget.h @@ -0,0 +1,122 @@ +/* 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, public ResourceDownloadRow::DownloadListener{ + Q_OBJECT +public: + using ParentOption = ResourceDownloadButton; + +public: + ~DownloadButtonWidget(); + DownloadButtonWidget(QWidget& parent, ResourceDownloadButton& value); + + virtual void on_metadata_fetch_finished(const std::string& popup_message) override; + virtual void on_exception_caught(const std::string& function_name) override; + virtual void on_download_failed() override; + virtual void on_action_state_updated() override; + +private: + void update_UI_state(); + void show_download_confirm_box( + const std::string& title, + const std::string& message_body + ); + +private: + ResourceDownloadButton& m_value; + ResourceDownloadRow& m_row; + QPushButton* m_button; + +}; + +void show_error_box(std::string function_name); + + +class DeleteButtonWidget : public QWidget, public ConfigWidget, public ResourceDownloadRow::DownloadListener{ +public: + using ParentOption = ResourceDeleteButton; + +public: + ~DeleteButtonWidget(); + DeleteButtonWidget(QWidget& parent, ResourceDeleteButton& value); + + virtual void on_action_state_updated() override; + +private: + void update_UI_state(); + void show_delete_confirm_box(); + +private: + ResourceDeleteButton& m_value; + ResourceDownloadRow& m_row; + QPushButton* m_button; +}; + +class CancelButtonWidget : public QWidget, public ConfigWidget, public ResourceDownloadRow::DownloadListener{ +public: + using ParentOption = ResourceCancelButton; + +public: + ~CancelButtonWidget(); + CancelButtonWidget(QWidget& parent, ResourceCancelButton& value); + + virtual void on_action_state_updated() override; + +private: + void update_UI_state(); + void show_cancel_confirm_box(); + +private: + ResourceCancelButton& m_value; + ResourceDownloadRow& m_row; + QPushButton* m_button; +}; + +class ProgressBarWidget : public QWidget, public ConfigWidget, public ResourceDownloadRow::DownloadListener{ +public: + using ParentOption = ResourceProgressBar; + +public: + ~ProgressBarWidget(); + ProgressBarWidget(QWidget& parent, ResourceProgressBar& value); + + virtual void on_download_progress(uint64_t bytes_done, uint64_t total_bytes) override; + virtual void on_unzip_progress(uint64_t bytes_done, uint64_t total_bytes) override; + virtual void on_hash_progress(uint64_t bytes_done, uint64_t total_bytes) override; + + virtual void on_action_state_updated() override; + +private: + void update_UI_state(); + void update_progress_bar(int percentage, const std::string& text); + void update_progress_bar(uint64_t bytes_done, uint64_t total_bytes, const std::string& text); + +private: + ResourceProgressBar& m_value; + ResourceDownloadRow& m_row; + QLabel* m_status_label; + QProgressBar* m_progress_bar; +}; + + + + +} +#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 76885d04e3..89a3cedb0e 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -489,6 +489,14 @@ file(GLOB LIBRARY_SOURCES 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