ENH: Open Prinables.com Links and Zip Archives (#3823)

* Enable ability to open `prusaslicer://` links

* Add needed function to miniz

* Import Zip Functionality

Allows zip file to be drag and dropped or imported via the menu option

Based on prusa3d/PrusaSlicer@ce38e57 and current master branch files

* Update dialog style to match Orca

* Ensure link is from printables

* add toggle option in preferences

doesn't actually control anything yet

* Add Downloader classes

As-is from PS master

* Create Orca Styled Variant of Icons

* Add Icons to ImGui

* Use PS's Downloader impl for `prusaslicer://` links

* Implement URL Registering on Windows

* Implement prusaslicer:// link on macOS

* Remove unnecessary class name qualifier in Plater.hpp

* Add downloader desktop integration registration and undo

* Revert Info.plist

---------

Co-authored-by: SoftFever <softfeverever@gmail.com>
This commit is contained in:
Ocraftyone 2024-05-21 22:52:34 -04:00 committed by GitHub
parent 0dbf610226
commit a764d836e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 3109 additions and 41 deletions

View file

@ -156,6 +156,7 @@
#include <libslic3r/CutUtils.hpp>
#include <wx/glcanvas.h> // Needs to be last because reasons :-/
#include <libslic3r/miniz_extension.hpp>
#include "WipeTowerDialog.hpp"
#include "ObjColorDialog.hpp"
@ -168,6 +169,7 @@
#include "PlateSettingsDialog.hpp"
#include "DailyTips.hpp"
#include "CreatePresetsDialog.hpp"
#include "FileArchiveDialog.hpp"
using boost::optional;
namespace fs = boost::filesystem;
@ -8996,8 +8998,9 @@ void Plater::import_model_id(wxString download_info)
/* prepare project and profile */
boost::thread import_thread = Slic3r::create_thread([&percent, &cont, &cancel, &retry_count, max_retries, &msg, &target_path, &download_ok, download_url, &filename] {
NetworkAgent* m_agent = Slic3r::GUI::wxGetApp().getAgent();
if (!m_agent) return;
// Orca: NetworkAgent is not needed and only prevents this from running
// NetworkAgent* m_agent = Slic3r::GUI::wxGetApp().getAgent();
// if (!m_agent) return;
int res = 0;
unsigned int http_code;
@ -9148,7 +9151,11 @@ void Plater::import_model_id(wxString download_info)
if (download_ok) {
BOOST_LOG_TRIVIAL(trace) << "import_model_id: target_path = " << target_path.string();
/* load project */
this->load_project(target_path.wstring());
// Orca: If download is a zip file, treat it as if file has been drag and dropped on the plater
if (target_path.extension() == ".zip")
this->load_files(wxArrayString(1, target_path.string()));
else
this->load_project(target_path.wstring());
/*BBS set project info after load project, project info is reset in load project */
//p->project.project_model_id = model_id;
//p->project.project_design_id = design_id;
@ -9818,6 +9825,19 @@ void Plater::calib_VFA(const Calib_Params& params)
p->background_process.fff_print()->set_calib_params(params);
}
BuildVolume_Type Plater::get_build_volume_type() const { return p->bed.get_build_volume_type(); }
void Plater::import_zip_archive()
{
wxString input_file;
wxGetApp().import_zip(this, input_file);
if (input_file.empty())
return;
wxArrayString arr;
arr.Add(input_file);
load_files(arr);
}
void Plater::import_sl1_archive()
{
auto &w = get_ui_job_worker();
@ -10003,6 +10023,186 @@ std::vector<size_t> Plater::load_files(const std::vector<std::string>& input_fil
return p->load_files(paths, strategy, ask_multi);
}
bool Plater::preview_zip_archive(const boost::filesystem::path& archive_path)
{
//std::vector<fs::path> unzipped_paths;
std::vector<fs::path> non_project_paths;
std::vector<fs::path> project_paths;
try
{
mz_zip_archive archive;
mz_zip_zero_struct(&archive);
if (!open_zip_reader(&archive, archive_path.string())) {
// TRN %1% is archive path
std::string err_msg = GUI::format(_u8L("Loading of a ZIP archive on path %1% has failed."), archive_path.string());
throw Slic3r::FileIOError(err_msg);
}
mz_uint num_entries = mz_zip_reader_get_num_files(&archive);
mz_zip_archive_file_stat stat;
// selected_paths contains paths and its uncompressed size. The size is used to distinguish between files with same path.
std::vector<std::pair<fs::path, size_t>> selected_paths;
FileArchiveDialog dlg(static_cast<wxWindow*>(wxGetApp().mainframe), &archive, selected_paths);
if (dlg.ShowModal() == wxID_OK)
{
std::string archive_path_string = archive_path.string();
archive_path_string = archive_path_string.substr(0, archive_path_string.size() - 4);
fs::path archive_dir(wxStandardPaths::Get().GetTempDir().utf8_str().data());
for (auto& path_w_size : selected_paths) {
const fs::path& path = path_w_size.first;
size_t size = path_w_size.second;
// find path in zip archive
for (mz_uint i = 0; i < num_entries; ++i) {
if (mz_zip_reader_file_stat(&archive, i, &stat)) {
if (size != stat.m_uncomp_size) // size must fit
continue;
wxString wname = boost::nowide::widen(stat.m_filename);
std::string name = boost::nowide::narrow(wname);
fs::path archive_path(name);
std::string extra(1024, 0);
size_t extra_size = mz_zip_reader_get_filename_from_extra(&archive, i, extra.data(), extra.size());
if (extra_size > 0) {
archive_path = fs::path(extra.substr(0, extra_size));
name = archive_path.string();
}
if (archive_path.empty())
continue;
if (path != archive_path)
continue;
// decompressing
try
{
std::replace(name.begin(), name.end(), '\\', '/');
// rename if file exists
std::string filename = path.filename().string();
std::string extension = boost::filesystem::extension(path);
std::string just_filename = filename.substr(0, filename.size() - extension.size());
std::string final_filename = just_filename;
size_t version = 0;
while (fs::exists(archive_dir / (final_filename + extension)))
{
++version;
final_filename = just_filename + "(" + std::to_string(version) + ")";
}
filename = final_filename + extension;
fs::path final_path = archive_dir / filename;
std::string buffer((size_t)stat.m_uncomp_size, 0);
// Decompress action. We already has correct file index in stat structure.
mz_bool res = mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, (void*)buffer.data(), (size_t)stat.m_uncomp_size, 0);
if (res == 0) {
// TRN: First argument = path to file, second argument = error description
wxString error_log = GUI::format_wxstr(_L("Failed to unzip file to %1%: %2%"), final_path.string(), mz_zip_get_error_string(mz_zip_get_last_error(&archive)));
BOOST_LOG_TRIVIAL(error) << error_log;
show_error(nullptr, error_log);
break;
}
// write buffer to file
fs::fstream file(final_path, std::ios::out | std::ios::binary | std::ios::trunc);
file.write(buffer.c_str(), buffer.size());
file.close();
if (!fs::exists(final_path)) {
wxString error_log = GUI::format_wxstr(_L("Failed to find unzipped file at %1%. Unzipping of file has failed."), final_path.string());
BOOST_LOG_TRIVIAL(error) << error_log;
show_error(nullptr, error_log);
break;
}
BOOST_LOG_TRIVIAL(info) << "Unzipped " << final_path;
if (!boost::algorithm::iends_with(filename, ".3mf") && !boost::algorithm::iends_with(filename, ".amf")) {
non_project_paths.emplace_back(final_path);
break;
}
// if 3mf - read archive headers to find project file
if (/*(boost::algorithm::iends_with(filename, ".3mf") && !is_project_3mf(final_path.string())) ||*/
(boost::algorithm::iends_with(filename, ".amf") && !boost::algorithm::iends_with(filename, ".zip.amf"))) {
non_project_paths.emplace_back(final_path);
break;
}
project_paths.emplace_back(final_path);
break;
}
catch (const std::exception& e)
{
// ensure the zip archive is closed and rethrow the exception
close_zip_reader(&archive);
throw Slic3r::FileIOError(e.what());
}
}
}
}
close_zip_reader(&archive);
if (non_project_paths.size() + project_paths.size() != selected_paths.size())
BOOST_LOG_TRIVIAL(error) << "Decompresing of archive did not retrieve all files. Expected files: "
<< selected_paths.size()
<< " Decopressed files: "
<< non_project_paths.size() + project_paths.size();
} else {
close_zip_reader(&archive);
return false;
}
}
catch (const Slic3r::FileIOError& e) {
// zip reader should be already closed or not even opened
GUI::show_error(this, e.what());
return false;
}
// none selected
if (project_paths.empty() && non_project_paths.empty())
{
return false;
}
// 1 project file and some models - behave like drag n drop of 3mf and then load models
if (project_paths.size() == 1)
{
wxArrayString aux;
aux.Add(from_u8(project_paths.front().string()));
bool loaded3mf = load_files(aux);
load_files(non_project_paths, LoadStrategy::LoadModel);
boost::system::error_code ec;
if (loaded3mf) {
fs::remove(project_paths.front(), ec);
if (ec)
BOOST_LOG_TRIVIAL(error) << ec.message();
}
for (const fs::path& path : non_project_paths) {
// Delete file from temp file (path variable), it will stay only in app memory.
boost::system::error_code ec;
fs::remove(path, ec);
if (ec)
BOOST_LOG_TRIVIAL(error) << ec.message();
}
return true;
}
// load all projects and all models as geometry
load_files(project_paths, LoadStrategy::LoadModel);
load_files(non_project_paths, LoadStrategy::LoadModel);
for (const fs::path& path : project_paths) {
// Delete file from temp file (path variable), it will stay only in app memory.
boost::system::error_code ec;
fs::remove(path, ec);
if (ec)
BOOST_LOG_TRIVIAL(error) << ec.message();
}
for (const fs::path& path : non_project_paths) {
// Delete file from temp file (path variable), it will stay only in app memory.
boost::system::error_code ec;
fs::remove(path, ec);
if (ec)
BOOST_LOG_TRIVIAL(error) << ec.message();
}
return true;
}
class RadioBox;
class RadioSelector
{
@ -10341,7 +10541,7 @@ void ProjectDropDialog::on_dpi_changed(const wxRect& suggested_rect)
//BBS: remove GCodeViewer as seperate APP logic
bool Plater::load_files(const wxArrayString& filenames)
{
const std::regex pattern_drop(".*[.](stp|step|stl|oltp|obj|amf|3mf|svg)", std::regex::icase);
const std::regex pattern_drop(".*[.](stp|step|stl|oltp|obj|amf|3mf|svg|zip)", std::regex::icase);
const std::regex pattern_gcode_drop(".*[.](gcode|g)", std::regex::icase);
std::vector<fs::path> normal_paths;
@ -10431,6 +10631,21 @@ bool Plater::load_files(const wxArrayString& filenames)
}
}
// Orca: Iters through given paths and imports files from zip then remove zip from paths
// returns true if zip files were found
auto handle_zips = [this](vector<fs::path>& paths) { // NOLINT(*-no-recursion) - Recursion is intended and should be managed properly
bool res = false;
for (auto it = paths.begin(); it != paths.end();) {
if (boost::algorithm::iends_with(it->string(), ".zip")) {
res = true;
preview_zip_archive(*it);
it = paths.erase(it);
} else
it++;
}
return res;
};
switch (loadfiles_type) {
case LoadFilesType::Single3MF:
open_3mf_file(normal_paths[0]);
@ -10438,6 +10653,7 @@ bool Plater::load_files(const wxArrayString& filenames)
case LoadFilesType::SingleOther: {
Plater::TakeSnapshot snapshot(this, snapshot_label);
if (handle_zips(normal_paths)) return true;
if (load_files(normal_paths, LoadStrategy::LoadModel, false).empty()) { res = false; }
break;
}
@ -10453,6 +10669,9 @@ bool Plater::load_files(const wxArrayString& filenames)
case LoadFilesType::MultipleOther: {
Plater::TakeSnapshot snapshot(this, snapshot_label);
if (handle_zips(normal_paths)) {
if (normal_paths.empty()) return true;
}
if (load_files(normal_paths, LoadStrategy::LoadModel, true).empty()) { res = false; }
break;
}
@ -10471,6 +10690,9 @@ bool Plater::load_files(const wxArrayString& filenames)
open_3mf_file(first_file[0]);
if (load_files(tmf_file, LoadStrategy::LoadModel).empty()) { res = false; }
if (res && handle_zips(other_file)) {
if (normal_paths.empty()) return true;
}
if (load_files(other_file, LoadStrategy::LoadModel, false).empty()) { res = false; }
break;
default: break;