diff --git a/src/chat_gui/send_image_popup.cpp b/src/chat_gui/send_image_popup.cpp index 6a1de02a..107b3ccc 100644 --- a/src/chat_gui/send_image_popup.cpp +++ b/src/chat_gui/send_image_popup.cpp @@ -6,6 +6,9 @@ #include "../image_loader_qoi.hpp" #include "../image_loader_sdl_image.hpp" +#include +#include + #include // fwd @@ -163,7 +166,51 @@ void SendImagePopup::sendMemory( _on_send = std::move(on_send); _on_cancel = std::move(on_cancel); +} +bool SendImagePopup::sendFilePath( // file2 instead? + std::string_view file_path, + std::function&, std::string_view)>&& on_send, + std::function&& on_cancel +) { + original_raw = false; + if (file_path.empty() || !std::filesystem::exists(file_path)) { + return false; // error + } + + std::filesystem::path path_o{file_path}; + + const auto file_size = std::filesystem::file_size(path_o); + if (file_size <= 0 || file_size > 100*1024*1024) { // limit to 100mib for now + return false; // error + } + + File2RFile file2{file_path}; + if (!file2.isGood() || !file2.can_read) { + std::cerr << "filed to read file '" << file_path << "'\n"; + } + + { // copy paste data to memory + // inefficent + const auto data = file2.read(file_size, 0); + original_data = {data.ptr, data.ptr+data.size}; + } + + if (path_o.has_extension()) { + original_file_ext = path_o.extension().u8string(); + } + + if (!load()) { + std::cerr << "SIP: failed to load image from file '" << file_path << "'\n"; + reset(); + return false; + } + + _open_popup = true; + + _on_send = std::move(on_send); + _on_cancel = std::move(on_cancel); + return true; } void SendImagePopup::render(float time_delta) { diff --git a/src/chat_gui/send_image_popup.hpp b/src/chat_gui/send_image_popup.hpp index 1465b791..9a88d2cd 100644 --- a/src/chat_gui/send_image_popup.hpp +++ b/src/chat_gui/send_image_popup.hpp @@ -68,7 +68,12 @@ struct SendImagePopup { std::function&& on_cancel ); // from memory_raw - // from file_path + + bool sendFilePath( // file2 instead? + std::string_view file_path, + std::function&, std::string_view)>&& on_send, + std::function&& on_cancel + ); // call this each frame void render(float time_delta); diff --git a/src/chat_gui4.cpp b/src/chat_gui4.cpp index b0de4269..80a5c86a 100644 --- a/src/chat_gui4.cpp +++ b/src/chat_gui4.cpp @@ -784,12 +784,14 @@ float ChatGui4::render(float time_delta) { //const auto* mime_type = clipboardHasImage(); //ImGui::BeginDisabled(mime_type == nullptr); if (ImGui::Button("paste\nfile", {-FLT_MIN, 0})) { - const auto* mime_type = clipboardHasImage(); - if (mime_type != nullptr) { // making sure - pasteFile(mime_type); + if (const auto* imt = clipboardHasImage(); imt != nullptr) { // making sure + pasteFile(imt); + } else if (const auto* fpmt = clipboardHasFileList(); fpmt != nullptr) { + pasteFile(fpmt); } //} else if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) { } else if (ImGui::BeginPopupContextItem(nullptr, ImGuiMouseButton_Right)) { + // TODO: use list instead const static std::vector image_mime_types { // add apng? "image/png", @@ -833,7 +835,7 @@ float ChatGui4::render(float time_delta) { return 1000.f; // TODO: higher min fps? } -void ChatGui4::sendFilePath(const char* file_path) { +void ChatGui4::sendFilePath(std::string_view file_path) { const auto path = std::filesystem::path(file_path); if (_selected_contact && std::filesystem::is_regular_file(path)) { _rmm.sendFilePath(*_selected_contact, path.filename().generic_u8string(), path.generic_u8string()); @@ -1338,44 +1340,163 @@ bool ChatGui4::renderContactListContactSmall(const Contact3 c, const bool select } void ChatGui4::pasteFile(const char* mime_type) { - size_t data_size = 0; - void* data = SDL_GetClipboardData(mime_type, &data_size); + if (!_selected_contact.has_value()) { + return; + } - // if image + if (mimeIsImage(mime_type)) { + size_t data_size = 0; + void* data = SDL_GetClipboardData(mime_type, &data_size); - std::cout << "CG: pasted image of size " << data_size << " mime " << mime_type << "\n"; + std::cout << "CG: pasted image of size " << data_size << " mime " << mime_type << "\n"; - _sip.sendMemory( - static_cast(data), data_size, - [this](const auto& img_data, const auto file_ext) { - // create file name - // TODO: move this into sip - std::ostringstream tmp_file_name {"tomato_Image_", std::ios_base::ate}; - { - const auto now = std::chrono::system_clock::now(); - const auto ctime = std::chrono::system_clock::to_time_t(now); - tmp_file_name - << std::put_time(std::localtime(&ctime), "%F_%H-%M-%S") - << "." - << std::setfill('0') << std::setw(3) - << std::chrono::duration_cast(now.time_since_epoch() - std::chrono::duration_cast(now.time_since_epoch())).count() - << file_ext - ; + _sip.sendMemory( + static_cast(data), data_size, + [this](const auto& img_data, const auto file_ext) { + // create file name + // TODO: move this into sip + std::ostringstream tmp_file_name {"tomato_Image_", std::ios_base::ate}; + { + const auto now = std::chrono::system_clock::now(); + const auto ctime = std::chrono::system_clock::to_time_t(now); + tmp_file_name + << std::put_time(std::localtime(&ctime), "%F_%H-%M-%S") + << "." + << std::setfill('0') << std::setw(3) + << std::chrono::duration_cast(now.time_since_epoch() - std::chrono::duration_cast(now.time_since_epoch())).count() + << file_ext + ; + } + + std::cout << "tmp image path " << tmp_file_name.str() << "\n"; + + const std::filesystem::path tmp_send_file_path = "tmp_send_files"; + std::filesystem::create_directories(tmp_send_file_path); + const auto tmp_file_path = tmp_send_file_path / tmp_file_name.str(); + + std::ofstream(tmp_file_path, std::ios_base::out | std::ios_base::binary) + .write(reinterpret_cast(img_data.data()), img_data.size()); + + _rmm.sendFilePath(*_selected_contact, tmp_file_name.str(), tmp_file_path.generic_u8string()); + }, + [](){} + ); + SDL_free(data); // free data + } else if (mimeIsFileList(mime_type)) { + size_t data_size = 0; + void* data = SDL_GetClipboardData(mime_type, &data_size); + + std::cout << "CG: pasted file list of size " << data_size << " mime " << mime_type << "\n"; + + std::vector list; + if (mime_type == std::string_view{"text/uri-list"}) { + // lines starting with # are comments + // every line is a link + // line sep is CRLF + std::string_view list_body{reinterpret_cast(data), data_size}; + size_t start {0}; + bool comment {false}; + for (size_t i = 0; i < data_size; i++) { + if (list_body[i] == '\r' || list_body[i] == '\n') { + if (!comment && i - start > 0) { + list.push_back(list_body.substr(start, i - start)); + } + start = i+1; + comment = false; + } else if (i == start && list_body[i] == '#') { + comment = true; + } + } + if (!comment && start+1 < data_size) { + list.push_back(list_body.substr(start)); + } + } else if (mime_type == std::string_view{"text/x-moz-url"}) { + assert(false && "implement me"); + // every link line is followed by link-title line (for the prev link) + // line sep unclear ("text/*" should always be CRLF, but its not explicitly specified) + // (does not matter, we can account for both) + } + + // TODO: remove debug log + std::cout << "preprocessing:\n"; + for (const auto it : list) { + std::cout << " >" << it << "\n"; + } + + // now we need to potentially convert file uris to file paths + + for (auto it = list.begin(); it != list.end();) { + constexpr auto size_of_file_uri_prefix = std::string_view{"file://"}.size(); + if (it->size() > size_of_file_uri_prefix && it->substr(0, size_of_file_uri_prefix) == std::string_view{"file://"}) { + it->remove_prefix(size_of_file_uri_prefix); } - std::cout << "tmp image path " << tmp_file_name.str() << "\n"; + std::filesystem::path path(*it); + if (!std::filesystem::is_regular_file(path)) { + it = list.erase(it); + } else { + it++; + } + } - const std::filesystem::path tmp_send_file_path = "tmp_send_files"; - std::filesystem::create_directories(tmp_send_file_path); - const auto tmp_file_path = tmp_send_file_path / tmp_file_name.str(); + std::cout << "postprocessing:\n"; + for (const auto it : list) { + std::cout << " >" << it << "\n"; + } - std::ofstream(tmp_file_path, std::ios_base::out | std::ios_base::binary) - .write(reinterpret_cast(img_data.data()), img_data.size()); + sendFileList(list); - _rmm.sendFilePath(*_selected_contact, tmp_file_name.str(), tmp_file_path.generic_u8string()); - }, - [](){} - ); - SDL_free(data); // free data + SDL_free(data); // free data + } +} + +void ChatGui4::sendFileList(const std::vector& list) { + // TODO: file collection sip + if (list.size() > 1) { + for (const auto it : list) { + sendFilePath(it); + } + } else if (list.size() == 1) { + const auto path = std::filesystem::path(list.front()); + if (std::filesystem::is_regular_file(path)) { + if (!_sip.sendFilePath( + list.front(), + [this](const auto& img_data, const auto file_ext) { + // create file name + // TODO: only create file if changed or from memory + // TODO: move this into sip + std::ostringstream tmp_file_name {"tomato_Image_", std::ios_base::ate}; + { + const auto now = std::chrono::system_clock::now(); + const auto ctime = std::chrono::system_clock::to_time_t(now); + tmp_file_name + << std::put_time(std::localtime(&ctime), "%F_%H-%M-%S") + << "." + << std::setfill('0') << std::setw(3) + << std::chrono::duration_cast(now.time_since_epoch() - std::chrono::duration_cast(now.time_since_epoch())).count() + << file_ext + ; + } + + std::cout << "tmp image path " << tmp_file_name.str() << "\n"; + + const std::filesystem::path tmp_send_file_path = "tmp_send_files"; + std::filesystem::create_directories(tmp_send_file_path); + const auto tmp_file_path = tmp_send_file_path / tmp_file_name.str(); + + std::ofstream(tmp_file_path, std::ios_base::out | std::ios_base::binary) + .write(reinterpret_cast(img_data.data()), img_data.size()); + + _rmm.sendFilePath(*_selected_contact, tmp_file_name.str(), tmp_file_path.generic_u8string()); + }, + [](){} + )) { + // if sip fails to open the file + sendFilePath(list.front()); + } + } else { + // if not file (???) + } + } } diff --git a/src/chat_gui4.hpp b/src/chat_gui4.hpp index 032e211b..66e5c7ec 100644 --- a/src/chat_gui4.hpp +++ b/src/chat_gui4.hpp @@ -73,7 +73,8 @@ class ChatGui4 { float render(float time_delta); public: - void sendFilePath(const char* file_path); + void sendFilePath(std::string_view file_path); + void sendFileList(const std::vector& list); private: void renderMessageBodyText(Message3Registry& reg, const Message3 e); diff --git a/src/main_screen.cpp b/src/main_screen.cpp index 940f3fd4..d0f1c200 100644 --- a/src/main_screen.cpp +++ b/src/main_screen.cpp @@ -11,6 +11,7 @@ #include #include +#include MainScreen::MainScreen(SimpleConfigModel&& conf_, SDL_Renderer* renderer_, Theme& theme_, std::string save_path, std::string save_password, std::string new_username, std::vector plugins) : renderer(renderer_), @@ -143,10 +144,11 @@ MainScreen::~MainScreen(void) { bool MainScreen::handleEvent(SDL_Event& e) { if (e.type == SDL_EVENT_DROP_FILE) { std::cout << "DROP FILE: " << e.drop.data << "\n"; - cg.sendFilePath(e.drop.data); + _dopped_files.emplace_back(e.drop.data); + //cg.sendFilePath(e.drop.data); _render_interval = 1.f/60.f; // TODO: magic _time_since_event = 0.f; - return true; // TODO: forward return succ from sendFilePath() + return true; } if ( @@ -482,6 +484,17 @@ Screen* MainScreen::tick(float time_delta, bool& quit) { mts.iterate(); // compute (after mfs) + if (!_dopped_files.empty()) { + std::vector tmp_view; + for (const std::string& it : _dopped_files) { + tmp_view.push_back(std::string_view{it}); + } + + cg.sendFileList(tmp_view); + + _dopped_files.clear(); + } + _min_tick_interval = std::min( // HACK: pow by 1.6 to increase 50 -> ~500 (~522) // and it does not change 1 diff --git a/src/main_screen.hpp b/src/main_screen.hpp index f0fccb1b..2d469dc8 100644 --- a/src/main_screen.hpp +++ b/src/main_screen.hpp @@ -100,6 +100,8 @@ struct MainScreen final : public Screen { uint64_t _window_hidden_ts {0}; float _time_since_event {0.f}; + std::vector _dopped_files; + MainScreen(SimpleConfigModel&& conf_, SDL_Renderer* renderer_, Theme& theme_, std::string save_path, std::string save_password, std::string new_username, std::vector plugins); ~MainScreen(void); diff --git a/src/sdl_clipboard_utils.cpp b/src/sdl_clipboard_utils.cpp index 564a17c4..f00614b8 100644 --- a/src/sdl_clipboard_utils.cpp +++ b/src/sdl_clipboard_utils.cpp @@ -2,22 +2,67 @@ #include +#include #include +#include -const char* clipboardHasImage(void) { - const static std::vector image_mime_types { - "image/png", - "image/webp", - "image/gif", - "image/jpeg", - "image/bmp", - }; - - for (const char* mime_type : image_mime_types) { - if (SDL_HasClipboardData(mime_type) == SDL_TRUE) { - return mime_type; +static const char* clipboardHas(const std::vector& filter_mime_types) { + for (const auto& mime_type : filter_mime_types) { + // ASSERTS that stringview is null terminated + if (SDL_HasClipboardData(mime_type.data()) == SDL_TRUE) { + return mime_type.data(); } } return nullptr; } + +const static std::vector image_mime_types { + "image/svg+xml", + "image/jxl", + "image/avif", + "image/apng", + "image/webp", + "image/png", + "image/gif", + "image/jpeg", + "image/qoi", + "image/bmp", + // tiff? +}; + +const static std::vector file_list_mime_types { + "text/uri-list", + "text/x-moz-url", +}; + +const char* clipboardHasImage(void) { + return clipboardHas(image_mime_types); +} + +const char* clipboardHasFileList(void) { + return clipboardHas(file_list_mime_types); +} + +bool mimeIsImage(const char* mime_type) { + if (mime_type == nullptr) { + return false; + } + + std::string_view mt_sv {mime_type}; + auto it = std::find(image_mime_types.cbegin(), image_mime_types.cend(), mime_type); + + return it != image_mime_types.cend(); +} + +bool mimeIsFileList(const char* mime_type) { + if (mime_type == nullptr) { + return false; + } + + std::string_view mt_sv {mime_type}; + auto it = std::find(file_list_mime_types.cbegin(), file_list_mime_types.cend(), mime_type); + + return it != image_mime_types.cend(); +} + diff --git a/src/sdl_clipboard_utils.hpp b/src/sdl_clipboard_utils.hpp index 3e1fd52e..631a4a39 100644 --- a/src/sdl_clipboard_utils.hpp +++ b/src/sdl_clipboard_utils.hpp @@ -3,4 +3,10 @@ // returns the mimetype (c-string) of the image in the clipboard // or nullptr, if there is none const char* clipboardHasImage(void); +const char* clipboardHasFileList(void); + +bool mimeIsImage(const char* mime_type); +bool mimeIsFileList(const char* mime_type); + +// TODO: add is file