diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 28596e3fb9..e74b6c66ed 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -504,6 +504,8 @@ set(SLIC3R_GUI_SOURCES GUI/SMPhysicalPrinterDialog.cpp GUI/SSWCP.cpp GUI/SSWCP.hpp + GUI/WCPDownloadManager.cpp + GUI/WCPDownloadManager.hpp GUI/WebPresetDialog.hpp GUI/WebPresetDialog.cpp GUI/WebSMUserLoginDialog.cpp diff --git a/src/slic3r/GUI/GUI.cpp b/src/slic3r/GUI/GUI.cpp index 6b338b5f03..5b6b619e86 100644 --- a/src/slic3r/GUI/GUI.cpp +++ b/src/slic3r/GUI/GUI.cpp @@ -549,6 +549,20 @@ void desktop_open_datadir_folder() #endif } +void desktop_open_any_folderEx(const std::string& path) +{ +#ifdef _WIN32 + // Convert path to Windows format (backslashes) and ensure it's properly quoted + boost::filesystem::path file_path(path); + file_path.make_preferred(); // Convert forward slashes to backslashes + wxString widepath = from_path(file_path); + // Quote the path to handle spaces and special characters + wxString cmd = L"explorer /select,\"" + widepath + L"\""; + ::wxExecute(cmd, wxEXEC_ASYNC, nullptr); +#else + desktop_open_any_folder(path); +#endif +} void desktop_open_any_folder( const std::string& path ) { // Execute command to open a file explorer, platform dependent. diff --git a/src/slic3r/GUI/GUI.hpp b/src/slic3r/GUI/GUI.hpp index db8cf06a61..acceaa8e4f 100644 --- a/src/slic3r/GUI/GUI.hpp +++ b/src/slic3r/GUI/GUI.hpp @@ -84,6 +84,9 @@ extern void login(); extern void desktop_open_datadir_folder(); // Ask the destop to open one folder extern void desktop_open_any_folder(const std::string& path); + +//open dir and select the file to show +extern void desktop_open_any_folderEx(const std::string& path); } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index efc2846661..5d8b699e55 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -13,6 +13,7 @@ #include "slic3r/GUI/WebPresetDialog.hpp" #include "slic3r/GUI/SSWCP.hpp" +#include "slic3r/GUI/WCPDownloadManager.hpp" #include "slic3r/Utils/PresetUpdater.hpp" #include "slic3r/Config/Version.hpp" @@ -1061,6 +1062,7 @@ GUI_App::GUI_App() , m_imgui(new ImGuiWrapper()) , m_removable_drive_manager(std::make_unique()) , m_downloader(std::make_unique()) + , m_wcp_download_manager(&WCPDownloadManager::getInstance()) , m_other_instance_message_handler(std::make_unique()) { //app config initializes early becasuse it is used in instance checking in Snapmaker_Orca.cpp @@ -6464,6 +6466,11 @@ Downloader* GUI_App::downloader() return m_downloader.get(); } +WCPDownloadManager* GUI_App::wcp_download_manager() +{ + return m_wcp_download_manager; +} + void GUI_App::load_url(wxString url) { if (mainframe) diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index 40c61dd0c1..38a950676a 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -89,6 +89,7 @@ class Plater; class ParamsPanel; class NotificationManager; class Downloader; +class WCPDownloadManager; struct GUI_InitParams; class ParamsDialog; class HMSQuery; @@ -297,6 +298,7 @@ private: size_t m_instance_hash_int; std::unique_ptr m_downloader; + WCPDownloadManager* m_wcp_download_manager; //BBS bool m_is_closing {false}; @@ -684,6 +686,7 @@ private: Model& model(); NotificationManager * notification_manager(); Downloader* downloader(); + WCPDownloadManager* wcp_download_manager(); std::string m_mall_model_download_url; diff --git a/src/slic3r/GUI/SSWCP.cpp b/src/slic3r/GUI/SSWCP.cpp index 57930222ed..33de5066f0 100644 --- a/src/slic3r/GUI/SSWCP.cpp +++ b/src/slic3r/GUI/SSWCP.cpp @@ -2,6 +2,7 @@ #include "SSWCP.hpp" #include "GUI_App.hpp" #include "MainFrame.hpp" +#include "WCPDownloadManager.hpp" #include "nlohmann/json.hpp" #include "slic3r/GUI/Tab.hpp" #include "sentry_wrapper/SentryWrapper.hpp" @@ -4297,6 +4298,12 @@ void SSWCP_UserLogin_Instance::process() sw_SubUserUpdatePrivacy(); } else if (m_cmd == GET_PRIVACY_STATUS) { sw_GetUserUpdatePrivacy(); + } else if (m_cmd == DOWNLOAD_FILE) { + sw_DownloadFile(); + } else if (m_cmd == CANCEL_DOWNLOAD) { + sw_CancelDownload(); + } else if (m_cmd == FILE_VIEW) { + sw_FileView(); } else { handle_general_fail(); @@ -4379,6 +4386,110 @@ void SSWCP_UserLogin_Instance::sw_GetUserUpdatePrivacy() } +void SSWCP_UserLogin_Instance::sw_DownloadFile() { + try { + std::string fileName = m_param_data.count("file_name") ? m_param_data["file_name"].get() : ""; + std::string fileUrl = m_param_data.count("file_url") ? m_param_data["file_url"].get() : ""; + + if (fileUrl.empty() || fileName.empty()) { + handle_general_fail(-1, "file_url and file_name are required"); + return; + } + + // Use WCP Download Manager + WCPDownloadManager* download_mgr = wxGetApp().wcp_download_manager(); + if (!download_mgr) { + handle_general_fail(-1, "WCP Download Manager not available"); + return; + } + + // Start download task + size_t task_id = download_mgr->start_download(fileUrl, fileName, shared_from_this()); + + // Return task ID to Flutter + json response; + response["task_id"] = task_id; + response["file_name"] = fileName; + response["file_url"] = fileUrl; + m_res_data = response; + m_status = 0; + m_msg = "Download started"; + send_to_js(); + // Note: Do not call finish_job() here, as download is asynchronous + // The manager will send progress updates and completion/error messages via WCP + + } catch (std::exception& e) { + handle_general_fail(-1, e.what()); + } +} + +void SSWCP_UserLogin_Instance::sw_CancelDownload() { + try { + size_t task_id = m_param_data.count("task_id") ? m_param_data["task_id"].get() : 0; + + if (task_id == 0) { + handle_general_fail(-1, "task_id is required"); + return; + } + + WCPDownloadManager* download_mgr = wxGetApp().wcp_download_manager(); + if (!download_mgr) { + handle_general_fail(-1, "WCP Download Manager not available"); + return; + } + + bool success = download_mgr->cancel_download(task_id); + + if (success) { + json response; + response["task_id"] = task_id; + response["canceled"] = true; + m_res_data = response; + m_status = 0; + m_msg = "Download canceled"; + } else { + handle_general_fail(-1, "Failed to cancel download or task not found"); + return; + } + + send_to_js(); + finish_job(); + } catch (std::exception& e) { + handle_general_fail(-1, e.what()); + } +} + +void SSWCP_UserLogin_Instance::sw_FileView() { + try { + std::string file_path = m_param_data.count("file_path") ? m_param_data["file_path"].get() : ""; + wxFileName file(file_path); + + if (!file.FileExists()) { + handle_general_fail(); + //wxMessageBox(wxT("file not exsit"), wxT("tips"), wxOK | wxICON_WARNING); + return; + } + + std::weak_ptr weak_self = shared_from_this(); + + wxGetApp().CallAfter([file_path, weak_self]() { + auto self = weak_self.lock(); + if (!self) { + return; + } + + //open file in folder + desktop_open_any_folderEx(file_path); + + self->send_to_js(); + self->finish_job(); + + }); + } catch (std::exception& e) { + handle_general_fail(); + } +} + void SSWCP_UserLogin_Instance::sw_SubUserUpdatePrivacy() { try { diff --git a/src/slic3r/GUI/SSWCP.hpp b/src/slic3r/GUI/SSWCP.hpp index 12717471ec..fe40a45a42 100644 --- a/src/slic3r/GUI/SSWCP.hpp +++ b/src/slic3r/GUI/SSWCP.hpp @@ -30,6 +30,9 @@ using tcp = asio::ip::tcp; #define UPLOAD_CAMERA_TIMELAPSE "sw_UploadCameraTimelapse" #define DELETE_CAMERA_TIMELAPSE "sw_DeleteCameraTimelapse" #define GET_DEVICEDATA_STORAGESPACE "sw_GetDeviceDataStorageSpace" +#define DOWNLOAD_FILE "sw_DownloadFile" +#define CANCEL_DOWNLOAD "sw_CancelDownload" +#define FILE_VIEW "sw_FileView" namespace Slic3r { namespace GUI { @@ -536,6 +539,11 @@ private: void sw_GetUserUpdatePrivacy(); void sw_SubUserUpdatePrivacy(); + + void sw_DownloadFile(); + void sw_CancelDownload(); + + void sw_FileView(); }; // Instance class for homepage business diff --git a/src/slic3r/GUI/WCPDownloadManager.cpp b/src/slic3r/GUI/WCPDownloadManager.cpp new file mode 100644 index 0000000000..a9d521aac3 --- /dev/null +++ b/src/slic3r/GUI/WCPDownloadManager.cpp @@ -0,0 +1,276 @@ +#include "WCPDownloadManager.hpp" +#include "GUI_App.hpp" +#include +#include +#include + +namespace Slic3r { namespace GUI { + +size_t WCPDownloadManager::start_download(const std::string& file_url, + const std::string& file_name, + std::shared_ptr wcp_instance) { + + std::lock_guard lock(m_tasks_mutex); + + size_t task_id = m_next_task_id++; + + // Get download path + auto downloadPath = wxGetApp().app_config->get("download_path"); + boost::filesystem::path dest_folder(downloadPath); + boost::filesystem::create_directories(dest_folder); + + boost::filesystem::path dest_file = dest_folder / file_name; + std::string dest_path = dest_file.string(); + + // Create task + auto task = std::make_shared(task_id, file_url, file_name, dest_path, wcp_instance); + task->state = WCPDownloadState::Downloading; + + m_tasks[task_id] = task; + + // Start download + wxGetApp().CallAfter([this, task]() { + try { + // Step 1: Create Http object + Http http = Http::get(task->file_url); + + // Step 2: Set progress callback + http.on_progress([this, task](Http::Progress progress, bool& cancel) { + if (task->state == WCPDownloadState::Canceled) { + cancel = true; + return; + } + + // Calculate progress + int percent = 0; + if (progress.dltotal > 0) { + percent = (int)(progress.dlnow * 100 / progress.dltotal); + } + + task->percent = percent; + + // Throttle progress updates: update every 5% or every second + std::lock_guard lock(m_tasks_mutex); + auto& last_pct = m_last_percent[task->task_id]; + auto& last_upd = m_last_update[task->task_id]; + + auto now = std::chrono::steady_clock::now(); + bool should_update = false; + + if (percent - last_pct >= 5) { + should_update = true; + last_pct = percent; + } else if (now - last_upd >= std::chrono::seconds(1)) { + should_update = true; + } + + if (should_update) { + last_upd = now; + wxGetApp().CallAfter([this, task, percent, progress]() { + send_progress_update(task, percent, progress.dlnow, progress.dltotal); + }); + } + }); + + // Step 3: Set complete callback + http.on_complete([this, task](std::string body, unsigned status) { + wxGetApp().CallAfter([this, task, body]() { + try { + // Save file + boost::nowide::ofstream file(task->dest_path, std::ios::binary); + if (!file.is_open()) { + send_error_update(task, "Failed to open file for writing"); + cleanup_task(task->task_id); + return; + } + + file.write(body.c_str(), body.size()); + file.close(); + + task->state = WCPDownloadState::Completed; + task->percent = 100; + send_complete_update(task, task->dest_path); + cleanup_task(task->task_id); + } catch (std::exception& e) { + send_error_update(task, e.what()); + cleanup_task(task->task_id); + } + }); + }); + + // Step 4: Set error callback + http.on_error([this, task](std::string body, std::string error, unsigned status) { + wxGetApp().CallAfter([this, task, error, status]() { + task->state = WCPDownloadState::Error; + task->error_message = error; + send_error_update(task, error); + cleanup_task(task->task_id); + }); + }); + + // Step 5: Start download and save Http::Ptr for cancellation + task->http_object = http.perform(); + + } catch (std::exception& e) { + task->state = WCPDownloadState::Error; + task->error_message = e.what(); + send_error_update(task, e.what()); + cleanup_task(task->task_id); + } + }); + + return task_id; +} + +bool WCPDownloadManager::cancel_download(size_t task_id) { + std::shared_ptr wcp_to_destroy; + + { + std::lock_guard lock(m_tasks_mutex); + + auto it = m_tasks.find(task_id); + if (it == m_tasks.end()) { + return false; + } + + auto task = it->second; + if (task->state == WCPDownloadState::Downloading) { + task->state = WCPDownloadState::Canceled; + if (task->http_object) { + task->http_object->cancel(); + } + + // Get WCP instance before cleanup (for destruction after lock release) + wcp_to_destroy = task->wcp_instance.lock(); + + cleanup_task(task_id); + } else { + return false; + } + } + + // Destroy WCP instance outside the lock to prevent deadlock + // This is the WCP instance from the original download request (sw_DownloadFile) + if (wcp_to_destroy) { + wcp_to_destroy->finish_job(); + } + + return true; +} + +bool WCPDownloadManager::pause_download(size_t task_id) { + // Pause functionality can be implemented if needed + // Current Http module may not support pause, need to implement resume from breakpoint + std::lock_guard lock(m_tasks_mutex); + auto it = m_tasks.find(task_id); + if (it != m_tasks.end() && it->second->state == WCPDownloadState::Downloading) { + it->second->state = WCPDownloadState::Paused; + // Note: Http module doesn't support pause directly, would need breakpoint resume + return true; + } + return false; +} + +bool WCPDownloadManager::resume_download(size_t task_id) { + // Resume functionality can be implemented if needed + // Would require breakpoint resume support in Http module + std::lock_guard lock(m_tasks_mutex); + auto it = m_tasks.find(task_id); + if (it != m_tasks.end() && it->second->state == WCPDownloadState::Paused) { + // Would need to restart download with range header + return false; // Not implemented yet + } + return false; +} + +WCPDownloadState WCPDownloadManager::get_task_state(size_t task_id) { + std::lock_guard lock(m_tasks_mutex); + auto it = m_tasks.find(task_id); + if (it != m_tasks.end()) { + return it->second->state; + } + return WCPDownloadState::Error; +} + +std::shared_ptr WCPDownloadManager::get_task(size_t task_id) { + std::lock_guard lock(m_tasks_mutex); + auto it = m_tasks.find(task_id); + if (it != m_tasks.end()) { + return it->second; + } + return nullptr; +} + +void WCPDownloadManager::send_progress_update(std::shared_ptr task, + int percent, + size_t downloaded, + size_t total) { + if (auto wcp = task->wcp_instance.lock()) { + json progress_data; + progress_data["task_id"] = task->task_id; + progress_data["percent"] = percent; + progress_data["downloaded"] = downloaded; + progress_data["total"] = total; + progress_data["state"] = "downloading"; + + wcp->m_res_data = progress_data; + wcp->m_status = 0; + wcp->m_msg = "Download progress"; + + // Use progress event ID + json header; + header["event_id"] = wcp->m_event_id + "_progress"; + header["command"] = "download_progress"; + wcp->m_header = header; + + wcp->send_to_js(); + } +} + +void WCPDownloadManager::send_complete_update(std::shared_ptr task, + const std::string& file_path) { + if (auto wcp = task->wcp_instance.lock()) { + json complete_data; + complete_data["task_id"] = task->task_id; + complete_data["file_path"] = file_path; + complete_data["file_name"] = task->file_name; + complete_data["percent"] = 100; + complete_data["state"] = "completed"; + + wcp->m_res_data = complete_data; + wcp->m_status = 0; + wcp->m_msg = "Download completed"; + wcp->send_to_js(); + + // Release WCP instance to prevent memory leak + wcp->finish_job(); + } +} + +void WCPDownloadManager::send_error_update(std::shared_ptr task, + const std::string& error) { + if (auto wcp = task->wcp_instance.lock()) { + json error_data; + error_data["task_id"] = task->task_id; + error_data["error"] = error; + error_data["state"] = "error"; + + wcp->m_res_data = error_data; + wcp->m_status = -1; + wcp->m_msg = error; + wcp->send_to_js(); + + // Release WCP instance to prevent memory leak + wcp->finish_job(); + } +} + +void WCPDownloadManager::cleanup_task(size_t task_id) { + std::lock_guard lock(m_tasks_mutex); + m_tasks.erase(task_id); + m_last_percent.erase(task_id); + m_last_update.erase(task_id); +} + +}} // namespace Slic3r::GUI + diff --git a/src/slic3r/GUI/WCPDownloadManager.hpp b/src/slic3r/GUI/WCPDownloadManager.hpp new file mode 100644 index 0000000000..fda468f777 --- /dev/null +++ b/src/slic3r/GUI/WCPDownloadManager.hpp @@ -0,0 +1,104 @@ +#ifndef slic3r_WCPDownloadManager_hpp_ +#define slic3r_WCPDownloadManager_hpp_ + +#include +#include +#include +#include +#include +#include +#include "../Utils/Http.hpp" +#include "SSWCP.hpp" +#include +#include "nlohmann/json.hpp" + +namespace Slic3r { namespace GUI { + +// Download task state +enum class WCPDownloadState { + Pending, + Downloading, + Paused, + Completed, + Error, + Canceled +}; + +// Download task information +struct WCPDownloadTask { + size_t task_id; + std::string file_url; + std::string file_name; + std::string dest_path; + std::weak_ptr wcp_instance; // Associated WCP instance + Http::Ptr http_object; // HTTP object for cancellation + WCPDownloadState state; + int percent; + std::string error_message; + + WCPDownloadTask(size_t id, const std::string& url, const std::string& name, + const std::string& path, std::shared_ptr instance) + : task_id(id), file_url(url), file_name(name), dest_path(path), + wcp_instance(instance), state(WCPDownloadState::Pending), percent(0) {} +}; + +// WCP Download Manager +class WCPDownloadManager { +public: + static WCPDownloadManager& getInstance() { + static WCPDownloadManager instance; + return instance; + } + + // Start a download task + size_t start_download(const std::string& file_url, + const std::string& file_name, + std::shared_ptr wcp_instance); + + // Cancel a download task + bool cancel_download(size_t task_id); + + // Pause a download task (if needed) + bool pause_download(size_t task_id); + + // Resume a download task (if needed) + bool resume_download(size_t task_id); + + // Get task state + WCPDownloadState get_task_state(size_t task_id); + + // Get task information + std::shared_ptr get_task(size_t task_id); + +private: + WCPDownloadManager() = default; + ~WCPDownloadManager() = default; + WCPDownloadManager(const WCPDownloadManager&) = delete; + WCPDownloadManager& operator=(const WCPDownloadManager&) = delete; + + std::mutex m_tasks_mutex; + std::unordered_map> m_tasks; + std::atomic m_next_task_id{1}; + + // Track last progress update for throttling + std::unordered_map m_last_percent; + std::unordered_map m_last_update; + + // Send progress update to WCP + void send_progress_update(std::shared_ptr task, int percent, + size_t downloaded, size_t total); + + // Send completion message to WCP + void send_complete_update(std::shared_ptr task, const std::string& file_path); + + // Send error message to WCP + void send_error_update(std::shared_ptr task, const std::string& error); + + // Clean up completed task + void cleanup_task(size_t task_id); +}; + +}} // namespace Slic3r::GUI + +#endif // slic3r_WCPDownloadManager_hpp_ +