diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index aa6268d88..ef7687f00 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -213,6 +213,8 @@ set(SLIC3R_GUI_SOURCES GUI/DesktopIntegrationDialog.hpp GUI/HintNotification.cpp GUI/HintNotification.hpp + Utils/AppUpdater.cpp + Utils/AppUpdater.hpp Utils/Http.cpp Utils/Http.hpp Utils/FixModelByWin10.cpp @@ -250,6 +252,7 @@ if (APPLE) list(APPEND SLIC3R_GUI_SOURCES Utils/RetinaHelperImpl.mm Utils/MacDarkMode.mm + Utils/MacUtils.mm GUI/RemovableDriveManagerMM.mm GUI/RemovableDriveManagerMM.h GUI/Mouse3DHandlerMac.mm diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index ee9199425..3c0d52bd1 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -57,6 +57,7 @@ #include "../Utils/PrintHost.hpp" #include "../Utils/Process.hpp" #include "../Utils/MacDarkMode.hpp" +#include "../Utils/AppUpdater.hpp" #include "slic3r/Config/Snapshot.hpp" #include "ConfigSnapshotDialog.hpp" #include "FirmwareDialog.hpp" @@ -816,18 +817,13 @@ void GUI_App::post_init() CallAfter([this] { bool cw_showed = this->config_wizard_startup(); this->preset_updater->sync(preset_bundle); + this->app_version_check(); if (! cw_showed) { // The CallAfter is needed as well, without it, GL extensions did not show. // Also, we only want to show this when the wizard does not, so the new user // sees something else than "we want something" on the first start. show_send_system_info_dialog_if_needed(); } - #ifdef _WIN32 - // Run external updater on Windows if version check is enabled. - if (this->preset_updater->version_check_enabled() && ! run_updater_win()) - // "prusaslicer-updater.exe" was not started, run our own update check. - #endif // _WIN32 - this->preset_updater->slic3r_update_notify(); }); } @@ -853,6 +849,8 @@ GUI_App::GUI_App(EAppMode mode) { //app config initializes early becasuse it is used in instance checking in PrusaSlicer.cpp this->init_app_config(); + // init app downloader after path to datadir is set + m_app_updater = std::make_unique(); } GUI_App::~GUI_App() @@ -1242,21 +1240,7 @@ bool GUI_App::on_init_inner() #endif // __WXMSW__ preset_updater = new PresetUpdater(); - Bind(EVT_SLIC3R_VERSION_ONLINE, [this](const wxCommandEvent& evt) { - app_config->set("version_online", into_u8(evt.GetString())); - app_config->save(); - std::string opt = app_config->get("notify_release"); - if (this->plater_ != nullptr && (opt == "all" || opt == "release")) { - if (*Semver::parse(SLIC3R_VERSION) < *Semver::parse(into_u8(evt.GetString()))) { - this->plater_->get_notification_manager()->push_notification(NotificationType::NewAppAvailable - , NotificationManager::NotificationLevel::ImportantNotificationLevel - , Slic3r::format(_u8L("New release version %1% is available."), evt.GetString()) - , _u8L("See Download page.") - , [](wxEvtHandler* evnthndlr) {wxGetApp().open_web_page_localized("https://www.prusa3d.com/slicerweb"); return true; } - ); - } - } - }); + Bind(EVT_SLIC3R_VERSION_ONLINE, &GUI_App::on_version_read, this); Bind(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, [this](const wxCommandEvent& evt) { app_config->save(); if (this->plater_ != nullptr && app_config->get("notify_release") == "all") { @@ -1272,6 +1256,18 @@ bool GUI_App::on_init_inner() } } }); + Bind(EVT_SLIC3R_APP_DOWNLOAD_PROGRESS, [this](const wxCommandEvent& evt) { + if (this->plater_ != nullptr) + this->plater_->get_notification_manager()->set_download_progress_percentage((float)std::stoi(into_u8(evt.GetString())) / 100.f ); + }); + + Bind(EVT_SLIC3R_APP_DOWNLOAD_FAILED, [this](const wxCommandEvent& evt) { + if (this->plater_ != nullptr) + this->plater_->get_notification_manager()->close_notification_of_type(NotificationType::AppDownload); + show_error(nullptr, evt.GetString()); + }); + + } else { #ifdef __WXMSW__ @@ -2282,7 +2278,8 @@ void GUI_App::add_config_menu(wxMenuBar *menu) local_menu->Append(config_id_base + ConfigMenuWizard, config_wizard_name + dots, config_wizard_tooltip); local_menu->Append(config_id_base + ConfigMenuSnapshots, _L("&Configuration Snapshots") + dots, _L("Inspect / activate configuration snapshots")); local_menu->Append(config_id_base + ConfigMenuTakeSnapshot, _L("Take Configuration &Snapshot"), _L("Capture a configuration snapshot")); - local_menu->Append(config_id_base + ConfigMenuUpdate, _L("Check for Configuration Updates"), _L("Check for configuration updates")); + local_menu->Append(config_id_base + ConfigMenuUpdateConf, _L("Check for Configuration Updates"), _L("Check for configuration updates")); + local_menu->Append(config_id_base + ConfigMenuUpdateApp, _L("Check for Application Updates"), _L("Check for new version of application")); #if defined(__linux__) && defined(SLIC3R_DESKTOP_INTEGRATION) //if (DesktopIntegrationDialog::integration_possible()) local_menu->Append(config_id_base + ConfigMenuDesktopIntegration, _L("Desktop Integration"), _L("Desktop Integration")); @@ -2324,9 +2321,12 @@ void GUI_App::add_config_menu(wxMenuBar *menu) case ConfigMenuWizard: run_wizard(ConfigWizard::RR_USER); break; - case ConfigMenuUpdate: + case ConfigMenuUpdateConf: check_updates(true); break; + case ConfigMenuUpdateApp: + app_updater(true); + break; #ifdef __linux__ case ConfigMenuDesktopIntegration: show_desktop_integration_dialog(); @@ -3266,5 +3266,108 @@ void GUI_App::associate_gcode_files() } #endif // __WXMSW__ + +void GUI_App::on_version_read(wxCommandEvent& evt) +{ + + app_config->set("version_online", into_u8(evt.GetString())); + app_config->save(); + std::string opt = app_config->get("notify_release"); + if (this->plater_ == nullptr || (opt != "all" && opt != "release")) { + return; + } + if (*Semver::parse(SLIC3R_VERSION) >= *Semver::parse(into_u8(evt.GetString()))) { + return; + } + // notification + /* + this->plater_->get_notification_manager()->push_notification(NotificationType::NewAppAvailable + , NotificationManager::NotificationLevel::ImportantNotificationLevel + , Slic3r::format(_u8L("New release version %1% is available."), evt.GetString()) + , _u8L("See Download page.") + , [](wxEvtHandler* evnthndlr) {wxGetApp().open_web_page_localized("https://www.prusa3d.com/slicerweb"); return true; } + ); + */ + // updater + + app_updater(false); +} + +void GUI_App::app_updater(bool from_user) +{ + DownloadAppData app_data = m_app_updater->get_app_data(); + + if (from_user && (!app_data.version || *app_data.version <= *Semver::parse(SLIC3R_VERSION))) + { + BOOST_LOG_TRIVIAL(info) << "There is no newer version online."; + MsgNoAppUpdates no_update_dialog; + no_update_dialog.ShowModal(); + return; + + } + + assert(!app_data.url.empty()); + assert(!app_data.target_path.empty()); + + // dialog with new version info + AppUpdateAvailableDialog dialog(*Semver::parse(SLIC3R_VERSION), *app_data.version); + auto dialog_result = dialog.ShowModal(); + // checkbox "do not show again" + if (dialog.disable_version_check()) { + app_config->set("notify_release", "none"); + } + // Doesn't wish to update + if (dialog_result != wxID_OK) { + return; + } + // dialog with new version download (installer or app dependent on system) + AppUpdateDownloadDialog dwnld_dlg(*app_data.version); + dialog_result = dwnld_dlg.ShowModal(); + // Doesn't wish to download + if (dialog_result != wxID_OK) { + return; + } + // Save as dialog + if (dwnld_dlg.select_download_path()) { + std::string extension = app_data.target_path.filename().extension().string(); + wxString wildcard; + if (!extension.empty()) { + extension = extension.substr(1); + wxString wxext = boost::nowide::widen(extension); + wildcard = GUI::format_wxstr("%1% Files (*.%2%)|*.%2%", wxext.Upper(), wxext); + } + wxFileDialog save_dlg( + plater() + , _L("Save as:") + , boost::nowide::widen(m_app_updater->get_default_dest_folder()) + , boost::nowide::widen(AppUpdater::get_filename_from_url(app_data.url)) + , wildcard + , wxFD_SAVE | wxFD_OVERWRITE_PROMPT + ); + // Canceled + if (save_dlg.ShowModal() != wxID_OK) { + return; + // set path + } else { + app_data.target_path = boost::filesystem::path(save_dlg.GetPath().ToUTF8().data()); + } + } + if (boost::filesystem::exists(app_data.target_path)) + { + BOOST_LOG_TRIVIAL(error) << "App download: File on target path already exists and will be overwritten. Path: " << app_data.target_path; + } + // start download + this->plater_->get_notification_manager()->push_download_progress_notification(_utf8("Download"), std::bind(&AppUpdater::cancel_callback, this->m_app_updater.get())); + app_data.start_after = dwnld_dlg.run_after_download(); + m_app_updater->set_app_data(std::move(app_data)); + m_app_updater->sync_download(); +} + +void GUI_App::app_version_check() +{ + std::string version_check_url = app_config->version_check_url(); + m_app_updater->sync_version(version_check_url); +} + } // GUI } //Slic3r diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index f93728d2e..4df41c607 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -32,6 +32,7 @@ class PresetUpdater; class ModelObject; class PrintHostJobQueue; class Model; +class AppUpdater; namespace GUI{ @@ -81,7 +82,8 @@ enum ConfigMenuIDs { ConfigMenuWizard, ConfigMenuSnapshots, ConfigMenuTakeSnapshot, - ConfigMenuUpdate, + ConfigMenuUpdateConf, + ConfigMenuUpdateApp, ConfigMenuDesktopIntegration, ConfigMenuPreferences, ConfigMenuModeSimple, @@ -156,6 +158,7 @@ private: std::unique_ptr m_imgui; std::unique_ptr m_printhost_job_queue; std::unique_ptr m_other_instance_message_handler; + std::unique_ptr m_app_updater; std::unique_ptr m_single_instance_checker; std::string m_instance_hash_string; size_t m_instance_hash_int; @@ -360,6 +363,11 @@ private: // Returns true if the configuration is fine. // Returns true if the configuration is not compatible and the user decided to rather close the slicer instead of reconfiguring. bool check_updates(const bool verbose); + void on_version_read(wxCommandEvent& evt); + // if the data from version file are already downloaded, shows dialogs to start download of new version of app + void app_updater(bool from_user); + // inititate read of version file online in separate thread + void app_version_check(); bool m_datadir_redefined { false }; }; diff --git a/src/slic3r/GUI/NotificationManager.cpp b/src/slic3r/GUI/NotificationManager.cpp index 6e9bc267f..9374ea17f 100644 --- a/src/slic3r/GUI/NotificationManager.cpp +++ b/src/slic3r/GUI/NotificationManager.cpp @@ -819,6 +819,120 @@ void NotificationManager::ProgressBarNotification::render_bar(ImGuiWrapper& imgu imgui.text(text.c_str()); } } +//------ProgressBarWithCancelNotification---------------- + +void NotificationManager::ProgressBarWithCancelNotification::render_close_button(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + if (m_percentage < 0.f || m_percentage >= 1.f) + render_close_button_inner(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); + else + render_cancel_button_inner(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); +} +void NotificationManager::ProgressBarWithCancelNotification::render_close_button_inner(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + ImVec2 win_size(win_size_x, win_size_y); + ImVec2 win_pos(win_pos_x, win_pos_y); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f)); + push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f)); + + + std::string button_text; + button_text = ImGui::CloseNotifButton; + + if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - win_size.x / 10.f, win_pos.y), + ImVec2(win_pos.x, win_pos.y + win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0)), + true)) + { + button_text = ImGui::CloseNotifHoverButton; + } + ImVec2 button_pic_size = ImGui::CalcTextSize(button_text.c_str()); + ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f); + ImGui::SetCursorPosX(win_size.x - m_line_height * 2.75f); + ImGui::SetCursorPosY(win_size.y / 2 - button_size.y); + if (imgui.button(button_text.c_str(), button_size.x, button_size.y)) + { + close(); + } + + //invisible large button + ImGui::SetCursorPosX(win_size.x - m_line_height * 2.35f); + ImGui::SetCursorPosY(0); + if (imgui.button(" ", m_line_height * 2.125, win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0))) + { + close(); + } + ImGui::PopStyleColor(5); + +} + +void NotificationManager::ProgressBarWithCancelNotification::render_cancel_button_inner(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + ImVec2 win_size(win_size_x, win_size_y); + ImVec2 win_pos(win_pos_x, win_pos_y); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(.0f, .0f, .0f, .0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(.0f, .0f, .0f, .0f)); + push_style_color(ImGuiCol_Text, ImVec4(1.f, 1.f, 1.f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + push_style_color(ImGuiCol_TextSelectedBg, ImVec4(0, .75f, .75f, 1.f), m_state == EState::FadingOut, m_current_fade_opacity); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(.0f, .0f, .0f, .0f)); + + + std::string button_text; + button_text = ImGui::CancelButton; + + if (ImGui::IsMouseHoveringRect(ImVec2(win_pos.x - win_size.x / 10.f, win_pos.y), + ImVec2(win_pos.x, win_pos.y + win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0)), + true)) + { + button_text = ImGui::CancelHoverButton; + } + ImVec2 button_pic_size = ImGui::CalcTextSize(button_text.c_str()); + ImVec2 button_size(button_pic_size.x * 1.25f, button_pic_size.y * 1.25f); + ImGui::SetCursorPosX(win_size.x - m_line_height * 2.75f); + ImGui::SetCursorPosY(win_size.y / 2 - button_size.y); + if (imgui.button(button_text.c_str(), button_size.x, button_size.y)) + { + on_cancel_button(); + } + + //invisible large button + ImGui::SetCursorPosX(win_size.x - m_line_height * 2.35f); + ImGui::SetCursorPosY(0); + if (imgui.button(" ", m_line_height * 2.125, win_size.y - (m_minimize_b_visible ? 2 * m_line_height : 0))) + { + on_cancel_button(); + } + ImGui::PopStyleColor(5); + +} + +void NotificationManager::ProgressBarWithCancelNotification::on_cancel_button() +{ + if (m_cancel_callback) { + if (m_cancel_callback()) { + close(); + } + } +} + +void NotificationManager::ProgressBarWithCancelNotification::render_bar(ImGuiWrapper& imgui, const float win_size_x, const float win_size_y, const float win_pos_x, const float win_pos_y) +{ + ProgressBarNotification::render_bar(imgui, win_size_x, win_size_y, win_pos_x, win_pos_y); + std::string text; + if (m_percentage < 0.f) { + text = _u8L("ERROR"); + } else { + std::stringstream stream; + stream << std::fixed << std::setprecision(2) << (int)(m_percentage * 100) << "%"; + text = stream.str(); + } + ImGui::SetCursorPosX(m_left_indentation); + ImGui::SetCursorPosY(win_size_y / 2 + win_size_y / 6 - (m_multiline ? 0 : m_line_height / 4)); + imgui.text(text.c_str()); +} + //------PrintHostUploadNotification---------------- void NotificationManager::PrintHostUploadNotification::init() { @@ -1653,11 +1767,37 @@ void NotificationManager::upload_job_notification_show_error(int id, const std:: } } +void NotificationManager::push_download_progress_notification(const std::string& text, std::function cancel_callback) +{ + // If already exists, change text and reset progress + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::AppDownload) { + notification->update({ NotificationType::AppDownload, NotificationLevel::ProgressBarNotificationLevel, 10, text }); + auto* pbwcn = dynamic_cast(notification.get()); + pbwcn->set_percentage(0.0f); + pbwcn->set_cancel_callback(cancel_callback); + return; + } + } + // push new one + NotificationData data{ NotificationType::AppDownload, NotificationLevel::ProgressBarNotificationLevel, 10, text }; + push_notification_data(std::make_unique(data, m_id_provider, m_evt_handler, cancel_callback), 0); +} +void NotificationManager::set_download_progress_percentage(float percentage) +{ + for (std::unique_ptr& notification : m_pop_notifications) { + if (notification->get_type() == NotificationType::AppDownload) { + dynamic_cast(notification.get())->set_percentage(percentage); + return; + } + } +} + void NotificationManager::init_slicing_progress_notification(std::function cancel_callback) { for (std::unique_ptr& notification : m_pop_notifications) { if (notification->get_type() == NotificationType::SlicingProgress) { - dynamic_cast(notification.get())->set_cancel_callback(cancel_callback); + dynamic_cast(notification.get())->set_cancel_callback(cancel_callback); return; } } diff --git a/src/slic3r/GUI/NotificationManager.hpp b/src/slic3r/GUI/NotificationManager.hpp index 9265cb55e..6d0ac9ed4 100644 --- a/src/slic3r/GUI/NotificationManager.hpp +++ b/src/slic3r/GUI/NotificationManager.hpp @@ -78,6 +78,8 @@ enum class NotificationType ProgressBar, // Progress bar with info from Print Host Upload Queue dialog. PrintHostUpload, + // Progress bar of download next version app. + AppDownload, // Progress bar with cancel button, cannot be closed // On end of slicing and G-code processing (the full G-code preview is available), // contains a hyperlink to export the G-code to a removable media or hdd. @@ -203,6 +205,9 @@ public: void set_upload_job_notification_percentage(int id, const std::string& filename, const std::string& host, float percentage); void upload_job_notification_show_canceled(int id, const std::string& filename, const std::string& host); void upload_job_notification_show_error(int id, const std::string& filename, const std::string& host); + // Download App progress + void push_download_progress_notification(const std::string& text, std::function cancel_callback); + void set_download_progress_percentage(float percentage); // slicing progress void init_slicing_progress_notification(std::function cancel_callback); void set_slicing_progress_began(); @@ -459,7 +464,34 @@ private: }; - + class ProgressBarWithCancelNotification : public ProgressBarNotification + { + public: + ProgressBarWithCancelNotification(const NotificationData& n, NotificationIDProvider& id_provider, wxEvtHandler* evt_handler, std::function cancel_callback) + : ProgressBarNotification(n, id_provider, evt_handler) + , m_cancel_callback(cancel_callback) + { + } + void set_percentage(float percent) override { m_percentage = percent; if(m_percentage >= 1.f) m_state = EState::FadingOut; else m_state = EState::NotFading; } + void set_cancel_callback(std::function cancel_callback) { m_cancel_callback = cancel_callback; } + protected: + void render_close_button(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y) override; + void render_close_button_inner(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y); + void render_cancel_button_inner(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y); + void render_bar(ImGuiWrapper& imgui, + const float win_size_x, const float win_size_y, + const float win_pos_x, const float win_pos_y) override; + void on_cancel_button(); + + std::function m_cancel_callback; + long m_hover_time{ 0 }; + }; class PrintHostUploadNotification : public ProgressBarNotification { diff --git a/src/slic3r/GUI/UpdateDialogs.cpp b/src/slic3r/GUI/UpdateDialogs.cpp index f80975ce5..031d64037 100644 --- a/src/slic3r/GUI/UpdateDialogs.cpp +++ b/src/slic3r/GUI/UpdateDialogs.cpp @@ -88,6 +88,89 @@ bool MsgUpdateSlic3r::disable_version_check() const return cbox->GetValue(); } + wxSize AppUpdateAvailableDialog::AUAD_size; +// AppUpdater +AppUpdateAvailableDialog::AppUpdateAvailableDialog(const Semver& ver_current, const Semver& ver_online) + : MsgDialog(nullptr, _(L("App Update available")), wxString::Format(_(L("New version of %s is available.\nDo you wish to download it?")), SLIC3R_APP_NAME)) +{ + auto* versions = new wxFlexGridSizer(1, 0, VERT_SPACING); + versions->Add(new wxStaticText(this, wxID_ANY, _(L("Current version:")))); + versions->Add(new wxStaticText(this, wxID_ANY, ver_current.to_string())); + versions->Add(new wxStaticText(this, wxID_ANY, _(L("New version:")))); + versions->Add(new wxStaticText(this, wxID_ANY, ver_online.to_string())); + content_sizer->Add(versions); + content_sizer->AddSpacer(VERT_SPACING); + + cbox = new wxCheckBox(this, wxID_ANY, _(L("Don't notify about new releases any more"))); + content_sizer->Add(cbox); + content_sizer->AddSpacer(VERT_SPACING); + + AUAD_size = content_sizer->GetSize(); + + + add_button(wxID_CANCEL); + + if (auto* btn_ok = get_button(wxID_OK); btn_ok != NULL) { + btn_ok->SetLabel(_L("Next")); + } + + finalize(); +} + +AppUpdateAvailableDialog::~AppUpdateAvailableDialog() {} + + +bool AppUpdateAvailableDialog::disable_version_check() const +{ + return cbox->GetValue(); +} + +// AppUpdateDownloadDialog +AppUpdateDownloadDialog::AppUpdateDownloadDialog( const Semver& ver_online) + : MsgDialog(nullptr, _(L("App Update download")), wxString::Format(_(L("New version of %s is available.")), SLIC3R_APP_NAME)) +{ + + auto* versions = new wxFlexGridSizer(2, 0, VERT_SPACING); + versions->Add(new wxStaticText(this, wxID_ANY, _(L("New version:")))); + versions->Add(new wxStaticText(this, wxID_ANY, ver_online.to_string())); + content_sizer->Add(versions); + content_sizer->AddSpacer(VERT_SPACING); +#ifndef __linux__ + cbox_run = new wxCheckBox(this, wxID_ANY, _(L("Run installer after download."))); + content_sizer->Add(cbox_run); +#endif + content_sizer->AddSpacer(VERT_SPACING); + cbox_path = new wxCheckBox(this, wxID_ANY, _(L("Select path for downloaded file."))); + content_sizer->Add(cbox_path); + content_sizer->AddSpacer(VERT_SPACING); + + content_sizer->SetMinSize(AppUpdateAvailableDialog::AUAD_size); + + add_button(wxID_CANCEL); + + if (auto* btn_ok = get_button(wxID_OK); btn_ok != NULL) { + btn_ok->SetLabel(_L("Download")); + } + + finalize(); +} + +AppUpdateDownloadDialog::~AppUpdateDownloadDialog() {} + + +bool AppUpdateDownloadDialog::run_after_download() const +{ +#ifndef __linux__ + return cbox_run->GetValue(); +#endif + return false; +} + +bool AppUpdateDownloadDialog::select_download_path() const +{ + return cbox_path->GetValue(); +} + // MsgUpdateConfig MsgUpdateConfig::MsgUpdateConfig(const std::vector &updates, bool force_before_wizard/* = false*/) : @@ -314,5 +397,25 @@ MsgNoUpdates::MsgNoUpdates() : MsgNoUpdates::~MsgNoUpdates() {} +// MsgNoAppUpdates +MsgNoAppUpdates::MsgNoAppUpdates() : + MsgDialog(nullptr, _(L("App update")), _(L("No updates available")), wxICON_ERROR | wxOK) +{ + + auto* text = new wxStaticText(this, wxID_ANY, wxString::Format( + _(L( + "%s has no version updates available." + )), + SLIC3R_APP_NAME + )); + text->Wrap(CONTENT_WIDTH * wxGetApp().em_unit()); + content_sizer->Add(text); + content_sizer->AddSpacer(VERT_SPACING); + + finalize(); +} + +MsgNoAppUpdates::~MsgNoAppUpdates() {} + } } diff --git a/src/slic3r/GUI/UpdateDialogs.hpp b/src/slic3r/GUI/UpdateDialogs.hpp index 435a8ccbd..fbc21a558 100644 --- a/src/slic3r/GUI/UpdateDialogs.hpp +++ b/src/slic3r/GUI/UpdateDialogs.hpp @@ -37,6 +37,42 @@ private: }; +class AppUpdateAvailableDialog : public MsgDialog +{ +public: + AppUpdateAvailableDialog(const Semver& ver_current, const Semver& ver_online); + AppUpdateAvailableDialog(AppUpdateAvailableDialog&&) = delete; + AppUpdateAvailableDialog(const AppUpdateAvailableDialog&) = delete; + AppUpdateAvailableDialog& operator=(AppUpdateAvailableDialog&&) = delete; + AppUpdateAvailableDialog& operator=(const AppUpdateAvailableDialog&) = delete; + virtual ~AppUpdateAvailableDialog(); + + // Tells whether the user checked the "don't bother me again" checkbox + bool disable_version_check() const; + static wxSize AUAD_size; +private: + wxCheckBox* cbox; +}; + +class AppUpdateDownloadDialog : public MsgDialog +{ +public: + AppUpdateDownloadDialog(const Semver& ver_online); + AppUpdateDownloadDialog(AppUpdateDownloadDialog&&) = delete; + AppUpdateDownloadDialog(const AppUpdateDownloadDialog&) = delete; + AppUpdateDownloadDialog& operator=(AppUpdateDownloadDialog&&) = delete; + AppUpdateDownloadDialog& operator=(const AppUpdateDownloadDialog&) = delete; + virtual ~AppUpdateDownloadDialog(); + + // Tells whether the user checked the "don't bother me again" checkbox + bool run_after_download() const; + bool select_download_path() const; + +private: + wxCheckBox* cbox_run; + wxCheckBox* cbox_path; +}; + // Confirmation dialog informing about configuration update. Lists updated bundles & their versions. class MsgUpdateConfig : public MsgDialog { @@ -129,6 +165,18 @@ public: ~MsgNoUpdates(); }; +// Informs about absence of new version online. +class MsgNoAppUpdates : public MsgDialog +{ +public: + MsgNoAppUpdates(); + MsgNoAppUpdates(MsgNoAppUpdates&&) = delete; + MsgNoAppUpdates(const MsgNoAppUpdates&) = delete; + MsgNoAppUpdates& operator=(MsgNoUpdates&&) = delete; + MsgNoAppUpdates& operator=(const MsgNoAppUpdates&) = delete; + ~MsgNoAppUpdates(); +}; + } } diff --git a/src/slic3r/Utils/AppUpdater.cpp b/src/slic3r/Utils/AppUpdater.cpp new file mode 100644 index 000000000..3d5e7a8ae --- /dev/null +++ b/src/slic3r/Utils/AppUpdater.cpp @@ -0,0 +1,694 @@ +#include "AppUpdater.hpp" + +#include +#include +#include +#include + +#include "slic3r/GUI/format.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/Utils/Http.hpp" + +#ifdef _WIN32 +#include +#include +#include +#include +#include +#endif // _WIN32 + + +namespace Slic3r { + +namespace { + +#ifdef _WIN32 + bool run_file(const boost::filesystem::path& path) + { + // find updater exe + if (boost::filesystem::exists(path)) { + // run updater. Original args: /silent -restartapp prusa-slicer.exe -startappfirst + + // Using quoted string as mentioned in CreateProcessW docs, silent execution parameter. + std::wstring wcmd = L"\"" + path.wstring(); + + // additional information + STARTUPINFOW si; + PROCESS_INFORMATION pi; + + // set the size of the structures + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + ZeroMemory(&pi, sizeof(pi)); + + // start the program up + if (CreateProcessW(NULL, // the path + wcmd.data(), // Command line + NULL, // Process handle not inheritable + NULL, // Thread handle not inheritable + FALSE, // Set handle inheritance to FALSE + 0, // No creation flags + NULL, // Use parent's environment block + NULL, // Use parent's starting directory + &si, // Pointer to STARTUPINFO structure + &pi // Pointer to PROCESS_INFORMATION structure (removed extra parentheses) + )) { + // Close process and thread handles. + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + return true; + } + else { + BOOST_LOG_TRIVIAL(error) << "Failed to run " << wcmd; + } + } + return false; + } + + std::string get_downloads_path() + { + std::string ret; + PWSTR path = NULL; + HRESULT hr = SHGetKnownFolderPath(FOLDERID_Downloads, 0, NULL, &path); + if (SUCCEEDED(hr)) { + ret = boost::nowide::narrow(path); + } + CoTaskMemFree(path); + return ret; + } + + bool open_folder(const boost::filesystem::path& path) + { + // this command can run the installer exe as well, but is it better than CreateProcessW? + ShellExecuteW(NULL, NULL, path.wstring().c_str(), NULL, NULL, SW_SHOWNORMAL); + return true; + } + +#elif __linux__ + bool run_file(const boost::filesystem::path& path) + { + return false; + } + + std::string get_downloads_path() + { + wxString command = "xdg-user-dir DOWNLOAD"; + wxArrayString output; + + //Check if we're running in an AppImage container, if so, we need to remove AppImage's env vars, + // because they may mess up the environment expected by the file manager. + // Mostly this is about LD_LIBRARY_PATH, but we remove a few more too for good measure. + if (wxGetEnv("APPIMAGE", nullptr)) { + // We're running from AppImage + wxEnvVariableHashMap env_vars; + wxGetEnvMap(&env_vars); + + env_vars.erase("APPIMAGE"); + env_vars.erase("APPDIR"); + env_vars.erase("LD_LIBRARY_PATH"); + env_vars.erase("LD_PRELOAD"); + env_vars.erase("UNION_PRELOAD"); + + wxExecuteEnv exec_env; + exec_env.env = std::move(env_vars); + + wxString owd; + if (wxGetEnv("OWD", &owd)) { + // This is the original work directory from which the AppImage image was run, + // set it as CWD for the child process: + exec_env.cwd = std::move(owd); + } + + ::wxExecute(command, output, 0, &exec_env); + + } else { + // Looks like we're NOT running from AppImage, we'll make no changes to the environment. + ::wxExecute(command, output); + } + if (output.GetCount() > 0) { + return boost::nowide::narrow(output[0]); + } + return std::string(); + } + + bool open_folder(const boost::filesystem::path& path) + { + if (boost::filesystem::is_directory(path)) { + const char *argv[] = { "xdg-open", path.string().c_str(), nullptr }; + + // Check if we're running in an AppImage container, if so, we need to remove AppImage's env vars, + // because they may mess up the environment expected by the file manager. + // Mostly this is about LD_LIBRARY_PATH, but we remove a few more too for good measure. + if (wxGetEnv("APPIMAGE", nullptr)) { + // We're running from AppImage + wxEnvVariableHashMap env_vars; + wxGetEnvMap(&env_vars); + + env_vars.erase("APPIMAGE"); + env_vars.erase("APPDIR"); + env_vars.erase("LD_LIBRARY_PATH"); + env_vars.erase("LD_PRELOAD"); + env_vars.erase("UNION_PRELOAD"); + + wxExecuteEnv exec_env; + exec_env.env = std::move(env_vars); + + wxString owd; + if (wxGetEnv("OWD", &owd)) { + // This is the original work directory from which the AppImage image was run, + // set it as CWD for the child process: + exec_env.cwd = std::move(owd); + } + + ::wxExecute(const_cast(argv), wxEXEC_ASYNC, nullptr, &exec_env); + + } else { + // Looks like we're NOT running from AppImage, we'll make no changes to the environment. + ::wxExecute(const_cast(argv), wxEXEC_ASYNC, nullptr, nullptr); + } + return true; + } + return false; + } + +#elif __APPLE__ + bool run_file(const boost::filesystem::path& path) + { + if (boost::filesystem::exists(path)) { + // attach downloaded dmg file + const char* argv1[] = { "hdiutil", "attach", path.string().c_str(), nullptr }; + ::wxExecute(const_cast(argv1), wxEXEC_ASYNC, nullptr); + // open inside attached as a folder in finder + const char* argv2[] = { "open", "/Volumes/PrusaSlicer", nullptr }; + ::wxExecute(const_cast(argv2), wxEXEC_ASYNC, nullptr); + return true; + } + return false; + } + + std::string get_downloads_path() + { + // call objective-c implementation + return get_downloads_path_mac(); + } + + bool open_folder(const boost::filesystem::path& path) + { + + if (boost::filesystem::is_directory(path)) { + const char* argv[] = { "open", path.string().c_str(), nullptr }; + ::wxExecute(const_cast(argv), wxEXEC_ASYNC, nullptr); + return true; + } + return false; + } +#endif +} // namespace + +wxDEFINE_EVENT(EVT_SLIC3R_VERSION_ONLINE, wxCommandEvent); +wxDEFINE_EVENT(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, wxCommandEvent); +wxDEFINE_EVENT(EVT_SLIC3R_APP_DOWNLOAD_PROGRESS, wxCommandEvent); +wxDEFINE_EVENT(EVT_SLIC3R_APP_DOWNLOAD_FAILED, wxCommandEvent); + +// priv handles all operations in separate thread +// 1) download version file and parse it. +// 2) download new app file and open in folder / run it. +struct AppUpdater::priv { + priv(); + // Download file. What happens with the data is specified in completefn. + bool http_get_file(const std::string& url + , size_t size_limit + , std::function progress_fn + , std::function completefn + , std::string& error_message + ) const; + + // Download installer / app + boost::filesystem::path download_file(const DownloadAppData& data) const; + // Run file in m_last_dest_path + bool run_downloaded_file(boost::filesystem::path path); + // gets version file via http + void version_check(const std::string& version_check_url); +#if 0 + // parsing of Prusaslicer.version2 + void parse_version_string_old(const std::string& body) const; +#endif + // parses ini tree of version file, saves to m_online_version_data and queue event(s) to UI + void parse_version_string(const std::string& body); + // thread + std::thread m_thread; + std::atomic_bool m_cancel; + std::mutex m_data_mutex; + // read only variable used to init m_online_version_data.target_path + boost::filesystem::path m_default_dest_folder; // readonly + // DownloadAppData read / write needs to be locked by m_data_mutex + DownloadAppData m_online_version_data; + DownloadAppData get_app_data(); + void set_app_data(DownloadAppData data); +}; + +AppUpdater::priv::priv() : + m_cancel (false) +#ifdef __linux__ + , m_default_dest_folder (boost::filesystem::path("/tmp")) +#else + , m_default_dest_folder (boost::filesystem::path(data_dir()) / "cache") +#endif //_WIN32 +{ + boost::filesystem::path downloads_path = boost::filesystem::path(get_downloads_path()); + if (!downloads_path.empty()) { + m_default_dest_folder = std::move(downloads_path); + } + BOOST_LOG_TRIVIAL(error) << "Default download path: " << m_default_dest_folder; + +} + +bool AppUpdater::priv::http_get_file(const std::string& url, size_t size_limit, std::function progress_fn, std::function complete_fn, std::string& error_message) const +{ + bool res = false; + Http::get(url) + .size_limit(size_limit) + .on_progress([&, progress_fn](Http::Progress progress, bool& cancel) { + // progress function returns true as success (to continue) + cancel = (this->m_cancel ? true : !progress_fn(std::move(progress))); + if (cancel) { + error_message = GUI::format("Error getting: `%1%`: Download was canceled.", + url); + BOOST_LOG_TRIVIAL(debug) << "AppUpdater::priv::http_get_file message: "<< error_message; + } + }) + .on_error([&](std::string body, std::string error, unsigned http_status) { + error_message = GUI::format("Error getting: `%1%`: HTTP %2%, %3%", + url, + http_status, + error); + BOOST_LOG_TRIVIAL(error) << error_message; + }) + .on_complete([&](std::string body, unsigned /* http_status */) { + assert(complete_fn != nullptr); + res = complete_fn(body, error_message); + }) + .perform_sync(); + + return res; +} + +boost::filesystem::path AppUpdater::priv::download_file(const DownloadAppData& data) const +{ + boost::filesystem::path dest_path; + size_t last_gui_progress = 0; + size_t expected_size = data.size; + dest_path = data.target_path; + assert(!dest_path.empty()); + if (dest_path.empty()) + { + BOOST_LOG_TRIVIAL(error) << "Download from " << data.url << " could not start. Destination path is empty."; + return boost::filesystem::path(); + } + std::string error_message; + bool res = http_get_file(data.url, 80 * 1024 * 1024 //TODO: what value here + // on_progress + , [&last_gui_progress, expected_size](Http::Progress progress) { + // size check + if (progress.dltotal > 0 && progress.dltotal > expected_size) { + std::string message = GUI::format("Downloading new %1% has failed. The file has incorrect file size. Aborting download.\nExpected size: %2%\nDownload size: %3%", SLIC3R_APP_NAME, expected_size, progress.dltotal); + BOOST_LOG_TRIVIAL(error) << message; + wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); + evt->SetString(message); + GUI::wxGetApp().QueueEvent(evt); + return false; + } else if (progress.dltotal > 0 && progress.dltotal < expected_size) { + BOOST_LOG_TRIVIAL(error) << GUI::format("Downloading new %1% has incorrect size. The download will continue. \nExpected size: %2%\nDownload size: %3%", SLIC3R_APP_NAME, expected_size, progress.dltotal);; + } + // progress event + size_t gui_progress = progress.dltotal > 0 ? 100 * progress.dlnow / progress.dltotal : 0; + //BOOST_LOG_TRIVIAL(error) << "App download " << gui_progress << "% " << progress.dlnow << " of " << progress.dltotal; + if (last_gui_progress < gui_progress && (last_gui_progress != 0 || gui_progress != 100)) { + last_gui_progress = gui_progress; + wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_PROGRESS); + evt->SetString(GUI::from_u8(std::to_string(gui_progress))); + GUI::wxGetApp().QueueEvent(evt); + } + return true; + } + // on_complete + , [dest_path, expected_size](std::string body, std::string& error_message){ + // Size check. Does always 1 char == 1 byte? + size_t body_size = body.size(); + if (body_size != expected_size) { + BOOST_LOG_TRIVIAL(error) << "Downloaded file has wrong size. Expected size: " << expected_size << " Downloaded size: " << body_size; + return false; + } + boost::filesystem::path tmp_path = dest_path; + tmp_path += format(".%1%%2%", get_current_pid(), ".download"); + try + { + boost::filesystem::fstream file(tmp_path, std::ios::out | std::ios::binary | std::ios::trunc); + file.write(body.c_str(), body.size()); + file.close(); + boost::filesystem::rename(tmp_path, dest_path); + } + catch (const std::exception&) + { + BOOST_LOG_TRIVIAL(error) << "Failed to write and move " << tmp_path << " to " << dest_path; + return false; + } + return true; + } + , error_message + ); + if (!res) + { + if (this->m_cancel) + { + BOOST_LOG_TRIVIAL(error) << error_message; + } else { + std::string message = GUI::format("Downloading new %1% has failed:\n%2%", SLIC3R_APP_NAME, error_message); + BOOST_LOG_TRIVIAL(error) << message; + wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_APP_DOWNLOAD_FAILED); + evt->SetString(message); + GUI::wxGetApp().QueueEvent(evt); + } + return boost::filesystem::path(); + } + + return dest_path; +} + +bool AppUpdater::priv::run_downloaded_file(boost::filesystem::path path) +{ + assert(!path.empty()); + bool res = run_file(path); + BOOST_LOG_TRIVIAL(error) << "Running "<< path.string() << " was " << res; + return res; +} + +void AppUpdater::priv::version_check(const std::string& version_check_url) +{ + assert(!version_check_url.empty()); + std::string error_message; + bool res = http_get_file(version_check_url, 1024 + // on_progress + , [](Http::Progress progress) { return true; } + // on_complete + , [&](std::string body, std::string& error_message) { + boost::trim(body); + parse_version_string(body); + return true; + } + , error_message + ); + if (!res) + BOOST_LOG_TRIVIAL(error) << "Failed to download version file: " << error_message; +} + +void AppUpdater::priv::parse_version_string(const std::string& body) +{ + size_t start = body.find('['); + if (start == std::string::npos) { +#if 0 + BOOST_LOG_TRIVIAL(error) << "Could not find property tree in version file. Starting old parsing."; + parse_version_string_old(body); + return; +#endif // 0 + BOOST_LOG_TRIVIAL(error) << "Could not find property tree in version file. Checking for application update has failed."; + return; + } + std::string tree_string = body.substr(start); + boost::property_tree::ptree tree; + std::stringstream ss(tree_string); + try { + boost::property_tree::read_ini(ss, tree); + } catch (const boost::property_tree::ini_parser::ini_parser_error& err) { + //throw Slic3r::RuntimeError(format("Failed reading version file property tree Error: \"%1%\" at line %2%. \nTree:\n%3%", err.message(), err.line(), tree_string).c_str()); + BOOST_LOG_TRIVIAL(error) << format("Failed reading version file property tree Error: \"%1%\" at line %2%. \nTree:\n%3%", err.message(), err.line(), tree_string); + return; + } + + DownloadAppData new_data; + + for (const auto& section : tree) { + std::string section_name = section.first; + + // online release version info + if (section_name == +#ifdef _WIN32 + "release:win64" +#elif __linux__ + "release:linux" +#else + "release:osx" +#endif + ) { + for (const auto& data : section.second) { + if (data.first == "url") { + new_data.url = data.second.data(); + new_data.target_path = m_default_dest_folder / AppUpdater::get_filename_from_url(new_data.url); + BOOST_LOG_TRIVIAL(error) << format("parsing version string: url: %1%", new_data.url); + } else if (data.first == "size"){ + new_data.size = std::stoi(data.second.data()); + BOOST_LOG_TRIVIAL(error) << format("parsing version string: expected size: %1%", new_data.size); + } + } + } + + // released versions - to be send to UI layer + if (section_name == "common") { + std::vector prerelease_versions; + for (const auto& data : section.second) { + // release version - save and send to UI layer + if (data.first == "release") { + std::string version = data.second.data(); + boost::optional release_version = Semver::parse(version); + if (!release_version) { + BOOST_LOG_TRIVIAL(error) << format("Received invalid contents from version file: Not a correct semver: `%1%`", version); + return; + } + new_data.version = release_version; + // Send after all data is read + /* + BOOST_LOG_TRIVIAL(info) << format("Got %1% online version: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version); + wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE); + evt->SetString(GUI::from_u8(version)); + GUI::wxGetApp().QueueEvent(evt); + */ + // prerelease versions - write down to be sorted and send to UI layer + } else if (data.first == "alpha") { + prerelease_versions.emplace_back(data.second.data()); + } else if (data.first == "beta") { + prerelease_versions.emplace_back(data.second.data()); + } else if (data.first == "rc") { + prerelease_versions.emplace_back(data.second.data()); + } + } + // find recent version that is newer than last full release. + boost::optional recent_version; + std::string version_string; + for (const std::string& ver_string : prerelease_versions) { + boost::optional ver = Semver::parse(ver_string); + if (ver && *new_data.version < *ver && ((recent_version && *recent_version < *ver) || !recent_version)) { + recent_version = ver; + version_string = ver_string; + } + } + // send prerelease version to UI layer + if (recent_version) { + BOOST_LOG_TRIVIAL(info) << format("Got %1% online version: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version_string); + wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE); + evt->SetString(GUI::from_u8(version_string)); + GUI::wxGetApp().QueueEvent(evt); + } + } + } + assert(!new_data.url.empty()); + assert(new_data.version); + // save + set_app_data(new_data); + // send + std::string version = new_data.version.get().to_string(); + BOOST_LOG_TRIVIAL(info) << format("Got %1% online version: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version); + wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE); + evt->SetString(GUI::from_u8(version)); + GUI::wxGetApp().QueueEvent(evt); +} + +#if 0 +void AppUpdater::priv::parse_version_string_old(const std::string& body) const +{ + + // release version + std::string version; + const auto first_nl_pos = body.find_first_of("\n\r"); + if (first_nl_pos != std::string::npos) + version = body.substr(0, first_nl_pos); + else + version = body; + boost::optional release_version = Semver::parse(version); + if (!release_version) { + BOOST_LOG_TRIVIAL(error) << format("Received invalid contents from `%1%`: Not a correct semver: `%2%`", SLIC3R_APP_NAME, version); + return; + } + BOOST_LOG_TRIVIAL(info) << format("Got %1% online version: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version); + wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_VERSION_ONLINE); + evt->SetString(GUI::from_u8(version)); + GUI::wxGetApp().QueueEvent(evt); + + // alpha / beta version + std::vector prerelease_versions; + size_t nexn_nl_pos = first_nl_pos; + while (nexn_nl_pos != std::string::npos && body.size() > nexn_nl_pos + 1) { + const auto last_nl_pos = nexn_nl_pos; + nexn_nl_pos = body.find_first_of("\n\r", last_nl_pos + 1); + std::string line; + if (nexn_nl_pos == std::string::npos) + line = body.substr(last_nl_pos + 1); + else + line = body.substr(last_nl_pos + 1, nexn_nl_pos - last_nl_pos - 1); + + // alpha + if (line.substr(0, 6) == "alpha=") { + version = line.substr(6); + if (!Semver::parse(version)) { + BOOST_LOG_TRIVIAL(error) << format("Received invalid contents for alpha release from `%1%`: Not a correct semver: `%2%`", SLIC3R_APP_NAME, version); + return; + } + prerelease_versions.emplace_back(version); + // beta + } + else if (line.substr(0, 5) == "beta=") { + version = line.substr(5); + if (!Semver::parse(version)) { + BOOST_LOG_TRIVIAL(error) << format("Received invalid contents for beta release from `%1%`: Not a correct semver: `%2%`", SLIC3R_APP_NAME, version); + return; + } + prerelease_versions.emplace_back(version); + } + } + // find recent version that is newer than last full release. + boost::optional recent_version; + for (const std::string& ver_string : prerelease_versions) { + boost::optional ver = Semver::parse(ver_string); + if (ver && *release_version < *ver && ((recent_version && *recent_version < *ver) || !recent_version)) { + recent_version = ver; + version = ver_string; + } + } + if (recent_version) { + BOOST_LOG_TRIVIAL(info) << format("Got %1% online version: `%2%`. Sending to GUI thread...", SLIC3R_APP_NAME, version); + wxCommandEvent* evt = new wxCommandEvent(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE); + evt->SetString(GUI::from_u8(version)); + GUI::wxGetApp().QueueEvent(evt); + } +} +#endif // 0 + +DownloadAppData AppUpdater::priv::get_app_data() +{ + const std::lock_guard lock(m_data_mutex); + DownloadAppData ret_val(m_online_version_data); + return ret_val; +} + +void AppUpdater::priv::set_app_data(DownloadAppData data) +{ + const std::lock_guard lock(m_data_mutex); + m_online_version_data = data; +} + +AppUpdater::AppUpdater() + :p(new priv()) +{ +} +AppUpdater::~AppUpdater() +{ + if (p && p->m_thread.joinable()) { + // This will stop transfers being done by the thread, if any. + // Cancelling takes some time, but should complete soon enough. + p->m_cancel = true; + p->m_thread.join(); + } +} +void AppUpdater::sync_download() +{ + assert(p); + // join thread first - it could have been in sync_version + if (p->m_thread.joinable()) { + // This will stop transfers being done by the thread, if any. + // Cancelling takes some time, but should complete soon enough. + p->m_cancel = true; + p->m_thread.join(); + } + p->m_cancel = false; + + DownloadAppData input_data = p->get_app_data(); + assert(!input_data.url.empty()); + + p->m_thread = std::thread( + [this, input_data]() { + if (boost::filesystem::path dest_path = p->download_file(input_data); boost::filesystem::exists(dest_path)){ + if (input_data.start_after) { + p->run_downloaded_file(std::move(dest_path)); + } else { + open_folder(dest_path.parent_path()); + } + } + }); +} + +void AppUpdater::sync_version(const std::string& version_check_url) +{ + assert(p); + // join thread first - it could have been in sync_download + if (p->m_thread.joinable()) { + // This will stop transfers being done by the thread, if any. + // Cancelling takes some time, but should complete soon enough. + p->m_cancel = true; + p->m_thread.join(); + } + p->m_cancel = false; + p->m_thread = std::thread( + [this, version_check_url]() { + p->version_check(version_check_url); + }); +} + +void AppUpdater::cancel() +{ + p->m_cancel = true; +} +bool AppUpdater::cancel_callback() +{ + cancel(); + return true; +} + +std::string AppUpdater::get_default_dest_folder() +{ + return p->m_default_dest_folder.string(); +} + +std::string AppUpdater::get_filename_from_url(const std::string& url) +{ + size_t slash = url.rfind('/'); + return (slash != std::string::npos ? url.substr(slash + 1) : url); +} + +std::string AppUpdater::get_file_extension_from_url(const std::string& url) +{ + size_t dot = url.rfind('.'); + return (dot != std::string::npos ? url.substr(dot) : url); +} + +void AppUpdater::set_app_data(DownloadAppData data) +{ + p->set_app_data(std::move(data)); +} + +DownloadAppData AppUpdater::get_app_data() +{ + return p->get_app_data(); +} + + +} //namespace Slic3r diff --git a/src/slic3r/Utils/AppUpdater.hpp b/src/slic3r/Utils/AppUpdater.hpp new file mode 100644 index 000000000..2cdcb900b --- /dev/null +++ b/src/slic3r/Utils/AppUpdater.hpp @@ -0,0 +1,62 @@ +#ifndef slic3r_AppUpdate_hpp_ +#define slic3r_AppUpdate_hpp_ + +#include +#include +#include "libslic3r/Utils.hpp" +#include "wx/event.h" + +//class boost::filesystem::path; + +namespace Slic3r { + +#ifdef __APPLE__ +// implmented at MacUtils.mm +std::string get_downloads_path_mac(); +#endif //__APPLE__ + +struct DownloadAppData +{ + std::string url; + bool start_after; + boost::optional version; + size_t size; + boost::filesystem::path target_path; +}; + +class AppUpdater +{ +public: + AppUpdater(); + ~AppUpdater(); + AppUpdater(AppUpdater&&) = delete; + AppUpdater(const AppUpdater&) = delete; + AppUpdater& operator=(AppUpdater&&) = delete; + AppUpdater& operator=(const AppUpdater&) = delete; + + // downloads app file + void sync_download(); + // downloads version file + void sync_version(const std::string& version_check_url); + void cancel(); + bool cancel_callback(); + + std::string get_default_dest_folder(); + + static std::string get_filename_from_url(const std::string& url); + static std::string get_file_extension_from_url(const std::string& url); + + // mutex access + void set_app_data(DownloadAppData data); + DownloadAppData get_app_data(); +private: + struct priv; + std::unique_ptr p; +}; + +wxDECLARE_EVENT(EVT_SLIC3R_VERSION_ONLINE, wxCommandEvent); +wxDECLARE_EVENT(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, wxCommandEvent); +wxDECLARE_EVENT(EVT_SLIC3R_APP_DOWNLOAD_PROGRESS, wxCommandEvent); +wxDECLARE_EVENT(EVT_SLIC3R_APP_DOWNLOAD_FAILED, wxCommandEvent); +} //namespace Slic3r +#endif diff --git a/src/slic3r/Utils/Http.cpp b/src/slic3r/Utils/Http.cpp index 63c26f721..68ddda041 100644 --- a/src/slic3r/Utils/Http.cpp +++ b/src/slic3r/Utils/Http.cpp @@ -207,7 +207,6 @@ size_t Http::priv::writecb(void *data, size_t size, size_t nmemb, void *userp) auto self = static_cast(userp); const char *cdata = static_cast(data); const size_t realsize = size * nmemb; - const size_t limit = self->limit > 0 ? self->limit : DEFAULT_SIZE_LIMIT; if (self->buffer.size() + realsize > limit) { // This makes curl_easy_perform return CURLE_WRITE_ERROR diff --git a/src/slic3r/Utils/MacUtils.mm b/src/slic3r/Utils/MacUtils.mm new file mode 100644 index 000000000..31a28f14e --- /dev/null +++ b/src/slic3r/Utils/MacUtils.mm @@ -0,0 +1,18 @@ +#import "AppUpdater.hpp" + +#import + +namespace Slic3r { + +// AppUpdater.hpp +std::string get_downloads_path_mac() +{ + // 1) + NSArray * paths = NSSearchPathForDirectoriesInDomains (NSDownloadsDirectory, NSUserDomainMask, YES); + NSString * desktopPath = [paths objectAtIndex:0]; + return std::string([desktopPath UTF8String]); + // 2) + //[NSURL fileURLWithPath:[NSHomeDirectory() stringByAppendingPathComponent:@"Downloads"]]; + //return std::string(); +} +} diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index 2b458df53..40f068155 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -136,8 +136,8 @@ struct Updates }; -wxDEFINE_EVENT(EVT_SLIC3R_VERSION_ONLINE, wxCommandEvent); -wxDEFINE_EVENT(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, wxCommandEvent); +//wxDEFINE_EVENT(EVT_SLIC3R_VERSION_ONLINE, wxCommandEvent); +//wxDEFINE_EVENT(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, wxCommandEvent); struct PresetUpdater::priv { @@ -162,8 +162,8 @@ struct PresetUpdater::priv void set_download_prefs(AppConfig *app_config); bool get_file(const std::string &url, const fs::path &target_path) const; void prune_tmps() const; - void sync_version() const; - void parse_version_string(const std::string& body) const; +// void sync_version() const; +// void parse_version_string(const std::string& body) const; void sync_config(const VendorMap vendors); void check_install_indices() const; @@ -238,6 +238,8 @@ void PresetUpdater::priv::prune_tmps() const } } +// moved to app updater + /* // Get Slic3rPE version available online, save in AppConfig. void PresetUpdater::priv::sync_version() const { @@ -257,7 +259,7 @@ void PresetUpdater::priv::sync_version() const http_status, error); }) - .on_complete([&](std::string body, unsigned /* http_status */) { + .on_complete([&](std::string body, unsigned ) { boost::trim(body); parse_version_string(body); }) @@ -268,6 +270,7 @@ void PresetUpdater::priv::sync_version() const // Version string must contain release version on first line. Follows non-mandatory alpha / beta releases on following lines (alpha=2.0.0-alpha1). void PresetUpdater::priv::parse_version_string(const std::string& body) const { + // release version std::string version; const auto first_nl_pos = body.find_first_of("\n\r"); @@ -331,8 +334,9 @@ void PresetUpdater::priv::parse_version_string(const std::string& body) const evt->SetString(GUI::from_u8(version)); GUI::wxGetApp().QueueEvent(evt); } + } - +*/ // Download vendor indices. Also download new bundles if an index indicates there's a new one available. // Both are saved in cache. void PresetUpdater::priv::sync_config(const VendorMap vendors) @@ -743,7 +747,7 @@ void PresetUpdater::sync(PresetBundle *preset_bundle) p->thread = std::thread([this, vendors]() { this->p->prune_tmps(); - this->p->sync_version(); +// this->p->sync_version(); this->p->sync_config(std::move(vendors)); }); } diff --git a/src/slic3r/Utils/PresetUpdater.hpp b/src/slic3r/Utils/PresetUpdater.hpp index 97d85a4ea..974a7dcfa 100644 --- a/src/slic3r/Utils/PresetUpdater.hpp +++ b/src/slic3r/Utils/PresetUpdater.hpp @@ -65,7 +65,7 @@ private: std::unique_ptr p; }; -wxDECLARE_EVENT(EVT_SLIC3R_VERSION_ONLINE, wxCommandEvent); -wxDECLARE_EVENT(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, wxCommandEvent); +//wxDECLARE_EVENT(EVT_SLIC3R_VERSION_ONLINE, wxCommandEvent); +//wxDECLARE_EVENT(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, wxCommandEvent); } #endif