forked from Green-Sky/tomato
rework mime types, support pasting file lists, SIP when dropping single files
This commit is contained in:
parent
73afcfaaeb
commit
a3c9be2348
@ -6,6 +6,9 @@
|
|||||||
#include "../image_loader_qoi.hpp"
|
#include "../image_loader_qoi.hpp"
|
||||||
#include "../image_loader_sdl_image.hpp"
|
#include "../image_loader_sdl_image.hpp"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <solanaceae/file/file2_std.hpp>
|
||||||
|
|
||||||
#include <imgui/imgui.h>
|
#include <imgui/imgui.h>
|
||||||
|
|
||||||
// fwd
|
// fwd
|
||||||
@ -163,7 +166,51 @@ void SendImagePopup::sendMemory(
|
|||||||
|
|
||||||
_on_send = std::move(on_send);
|
_on_send = std::move(on_send);
|
||||||
_on_cancel = std::move(on_cancel);
|
_on_cancel = std::move(on_cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SendImagePopup::sendFilePath( // file2 instead?
|
||||||
|
std::string_view file_path,
|
||||||
|
std::function<void(const std::vector<uint8_t>&, std::string_view)>&& on_send,
|
||||||
|
std::function<void(void)>&& 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) {
|
void SendImagePopup::render(float time_delta) {
|
||||||
|
@ -68,7 +68,12 @@ struct SendImagePopup {
|
|||||||
std::function<void(void)>&& on_cancel
|
std::function<void(void)>&& on_cancel
|
||||||
);
|
);
|
||||||
// from memory_raw
|
// from memory_raw
|
||||||
// from file_path
|
|
||||||
|
bool sendFilePath( // file2 instead?
|
||||||
|
std::string_view file_path,
|
||||||
|
std::function<void(const std::vector<uint8_t>&, std::string_view)>&& on_send,
|
||||||
|
std::function<void(void)>&& on_cancel
|
||||||
|
);
|
||||||
|
|
||||||
// call this each frame
|
// call this each frame
|
||||||
void render(float time_delta);
|
void render(float time_delta);
|
||||||
|
@ -784,12 +784,14 @@ float ChatGui4::render(float time_delta) {
|
|||||||
//const auto* mime_type = clipboardHasImage();
|
//const auto* mime_type = clipboardHasImage();
|
||||||
//ImGui::BeginDisabled(mime_type == nullptr);
|
//ImGui::BeginDisabled(mime_type == nullptr);
|
||||||
if (ImGui::Button("paste\nfile", {-FLT_MIN, 0})) {
|
if (ImGui::Button("paste\nfile", {-FLT_MIN, 0})) {
|
||||||
const auto* mime_type = clipboardHasImage();
|
if (const auto* imt = clipboardHasImage(); imt != nullptr) { // making sure
|
||||||
if (mime_type != nullptr) { // making sure
|
pasteFile(imt);
|
||||||
pasteFile(mime_type);
|
} else if (const auto* fpmt = clipboardHasFileList(); fpmt != nullptr) {
|
||||||
|
pasteFile(fpmt);
|
||||||
}
|
}
|
||||||
//} else if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
//} else if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
||||||
} else if (ImGui::BeginPopupContextItem(nullptr, ImGuiMouseButton_Right)) {
|
} else if (ImGui::BeginPopupContextItem(nullptr, ImGuiMouseButton_Right)) {
|
||||||
|
// TODO: use list instead
|
||||||
const static std::vector<const char*> image_mime_types {
|
const static std::vector<const char*> image_mime_types {
|
||||||
// add apng?
|
// add apng?
|
||||||
"image/png",
|
"image/png",
|
||||||
@ -833,7 +835,7 @@ float ChatGui4::render(float time_delta) {
|
|||||||
return 1000.f; // TODO: higher min fps?
|
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);
|
const auto path = std::filesystem::path(file_path);
|
||||||
if (_selected_contact && std::filesystem::is_regular_file(path)) {
|
if (_selected_contact && std::filesystem::is_regular_file(path)) {
|
||||||
_rmm.sendFilePath(*_selected_contact, path.filename().generic_u8string(), path.generic_u8string());
|
_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) {
|
void ChatGui4::pasteFile(const char* mime_type) {
|
||||||
size_t data_size = 0;
|
if (!_selected_contact.has_value()) {
|
||||||
void* data = SDL_GetClipboardData(mime_type, &data_size);
|
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(
|
_sip.sendMemory(
|
||||||
static_cast<const uint8_t*>(data), data_size,
|
static_cast<const uint8_t*>(data), data_size,
|
||||||
[this](const auto& img_data, const auto file_ext) {
|
[this](const auto& img_data, const auto file_ext) {
|
||||||
// create file name
|
// create file name
|
||||||
// TODO: move this into sip
|
// TODO: move this into sip
|
||||||
std::ostringstream tmp_file_name {"tomato_Image_", std::ios_base::ate};
|
std::ostringstream tmp_file_name {"tomato_Image_", std::ios_base::ate};
|
||||||
{
|
{
|
||||||
const auto now = std::chrono::system_clock::now();
|
const auto now = std::chrono::system_clock::now();
|
||||||
const auto ctime = std::chrono::system_clock::to_time_t(now);
|
const auto ctime = std::chrono::system_clock::to_time_t(now);
|
||||||
tmp_file_name
|
tmp_file_name
|
||||||
<< std::put_time(std::localtime(&ctime), "%F_%H-%M-%S")
|
<< std::put_time(std::localtime(&ctime), "%F_%H-%M-%S")
|
||||||
<< "."
|
<< "."
|
||||||
<< std::setfill('0') << std::setw(3)
|
<< std::setfill('0') << std::setw(3)
|
||||||
<< std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch() - std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch())).count()
|
<< std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch() - std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch())).count()
|
||||||
<< file_ext
|
<< 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<const char*>(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<std::string_view> 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<const char*>(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::cout << "postprocessing:\n";
|
||||||
std::filesystem::create_directories(tmp_send_file_path);
|
for (const auto it : list) {
|
||||||
const auto tmp_file_path = tmp_send_file_path / tmp_file_name.str();
|
std::cout << " >" << it << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
std::ofstream(tmp_file_path, std::ios_base::out | std::ios_base::binary)
|
sendFileList(list);
|
||||||
.write(reinterpret_cast<const char*>(img_data.data()), img_data.size());
|
|
||||||
|
|
||||||
_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<std::string_view>& 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<std::chrono::milliseconds>(now.time_since_epoch() - std::chrono::duration_cast<std::chrono::seconds>(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<const char*>(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 (???)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +73,8 @@ class ChatGui4 {
|
|||||||
float render(float time_delta);
|
float render(float time_delta);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
void sendFilePath(const char* file_path);
|
void sendFilePath(std::string_view file_path);
|
||||||
|
void sendFileList(const std::vector<std::string_view>& list);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void renderMessageBodyText(Message3Registry& reg, const Message3 e);
|
void renderMessageBodyText(Message3Registry& reg, const Message3 e);
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
MainScreen::MainScreen(SimpleConfigModel&& conf_, SDL_Renderer* renderer_, Theme& theme_, std::string save_path, std::string save_password, std::string new_username, std::vector<std::string> plugins) :
|
MainScreen::MainScreen(SimpleConfigModel&& conf_, SDL_Renderer* renderer_, Theme& theme_, std::string save_path, std::string save_password, std::string new_username, std::vector<std::string> plugins) :
|
||||||
renderer(renderer_),
|
renderer(renderer_),
|
||||||
@ -143,10 +144,11 @@ MainScreen::~MainScreen(void) {
|
|||||||
bool MainScreen::handleEvent(SDL_Event& e) {
|
bool MainScreen::handleEvent(SDL_Event& e) {
|
||||||
if (e.type == SDL_EVENT_DROP_FILE) {
|
if (e.type == SDL_EVENT_DROP_FILE) {
|
||||||
std::cout << "DROP FILE: " << e.drop.data << "\n";
|
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
|
_render_interval = 1.f/60.f; // TODO: magic
|
||||||
_time_since_event = 0.f;
|
_time_since_event = 0.f;
|
||||||
return true; // TODO: forward return succ from sendFilePath()
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -482,6 +484,17 @@ Screen* MainScreen::tick(float time_delta, bool& quit) {
|
|||||||
|
|
||||||
mts.iterate(); // compute (after mfs)
|
mts.iterate(); // compute (after mfs)
|
||||||
|
|
||||||
|
if (!_dopped_files.empty()) {
|
||||||
|
std::vector<std::string_view> 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<float>(
|
_min_tick_interval = std::min<float>(
|
||||||
// HACK: pow by 1.6 to increase 50 -> ~500 (~522)
|
// HACK: pow by 1.6 to increase 50 -> ~500 (~522)
|
||||||
// and it does not change 1
|
// and it does not change 1
|
||||||
|
@ -100,6 +100,8 @@ struct MainScreen final : public Screen {
|
|||||||
uint64_t _window_hidden_ts {0};
|
uint64_t _window_hidden_ts {0};
|
||||||
float _time_since_event {0.f};
|
float _time_since_event {0.f};
|
||||||
|
|
||||||
|
std::vector<std::string> _dopped_files;
|
||||||
|
|
||||||
MainScreen(SimpleConfigModel&& conf_, SDL_Renderer* renderer_, Theme& theme_, std::string save_path, std::string save_password, std::string new_username, std::vector<std::string> plugins);
|
MainScreen(SimpleConfigModel&& conf_, SDL_Renderer* renderer_, Theme& theme_, std::string save_path, std::string save_password, std::string new_username, std::vector<std::string> plugins);
|
||||||
~MainScreen(void);
|
~MainScreen(void);
|
||||||
|
|
||||||
|
@ -2,22 +2,67 @@
|
|||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
const char* clipboardHasImage(void) {
|
static const char* clipboardHas(const std::vector<std::string_view>& filter_mime_types) {
|
||||||
const static std::vector<const char*> image_mime_types {
|
for (const auto& mime_type : filter_mime_types) {
|
||||||
"image/png",
|
// ASSERTS that stringview is null terminated
|
||||||
"image/webp",
|
if (SDL_HasClipboardData(mime_type.data()) == SDL_TRUE) {
|
||||||
"image/gif",
|
return mime_type.data();
|
||||||
"image/jpeg",
|
|
||||||
"image/bmp",
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const char* mime_type : image_mime_types) {
|
|
||||||
if (SDL_HasClipboardData(mime_type) == SDL_TRUE) {
|
|
||||||
return mime_type;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const static std::vector<std::string_view> 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<std::string_view> 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -3,4 +3,10 @@
|
|||||||
// returns the mimetype (c-string) of the image in the clipboard
|
// returns the mimetype (c-string) of the image in the clipboard
|
||||||
// or nullptr, if there is none
|
// or nullptr, if there is none
|
||||||
const char* clipboardHasImage(void);
|
const char* clipboardHasImage(void);
|
||||||
|
const char* clipboardHasFileList(void);
|
||||||
|
|
||||||
|
bool mimeIsImage(const char* mime_type);
|
||||||
|
bool mimeIsFileList(const char* mime_type);
|
||||||
|
|
||||||
|
// TODO: add is file
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user