3#define WIN32_LEAN_AND_MEAN
15#include <system_error>
42 hr = CoInitializeEx(
nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
56 inline std::string
slugify(
const std::string& s)
59 out.reserve(s.size());
60 for (
unsigned char ch : s)
62 char c =
static_cast<char>(std::tolower(ch));
63 if (std::isalnum(
static_cast<unsigned char>(c)) || c ==
'.' || c ==
'_' || c ==
'-')
67 else if (std::isspace(
static_cast<unsigned char>(c)))
78 inline std::filesystem::path
uniqueName(
const std::filesystem::path& destDir,
79 const std::string& baseFile)
81 std::filesystem::path name = destDir / baseFile;
82 if (!std::filesystem::exists(name))
85 auto stem = std::filesystem::path(baseFile).stem().string();
86 auto ext = std::filesystem::path(baseFile).extension().string();
87 for (
int i = 1; i < 100000; ++i)
89 auto trial = destDir / (stem +
" (" + std::to_string(i) +
")" + ext);
90 if (!std::filesystem::exists(trial))
94 return destDir / (stem +
"_dup" + ext);
104 IFileOpenDialog* pDlg =
nullptr;
105 HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog,
nullptr, CLSCTX_INPROC_SERVER,
106 IID_PPV_ARGS(&pDlg));
107 if (FAILED(hr) || !pDlg)
111 COMDLG_FILTERSPEC filters[] = {
112 {L
"Images (*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.webp;*.tga)", L
"*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.webp;*.tga"},
113 {L
"All files (*.*)", L
"*.*"}};
114 pDlg->SetFileTypes(2, filters);
115 pDlg->SetFileTypeIndex(1);
116 pDlg->SetOptions(FOS_PATHMUSTEXIST | FOS_FILEMUSTEXIST);
118 std::optional<std::filesystem::path> result;
120 hr = pDlg->Show(
nullptr);
123 IShellItem* pItem =
nullptr;
124 if (SUCCEEDED(pDlg->GetResult(&pItem)) && pItem)
126 PWSTR pszFilePath =
nullptr;
127 if (SUCCEEDED(pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath)) && pszFilePath)
129 result = std::filesystem::path(pszFilePath);
130 CoTaskMemFree(pszFilePath);
141 std::filesystem::path* outDst =
nullptr,
142 std::string* outError =
nullptr)
145 if (!std::filesystem::exists(srcPath))
148 *outError =
"Source does not exist";
152 std::filesystem::create_directories(destRoot, ec);
156 auto filename = srcPath.filename().string();
157 auto ext = std::filesystem::path(filename).extension().string();
158 auto stem = std::filesystem::path(filename).stem().string();
159 std::string base =
slugify(stem) + ext;
162 std::filesystem::copy_file(srcPath, dst,
163 std::filesystem::copy_options::overwrite_existing, ec);
167 *outError =
"Copy failed: " + ec.message();
177 std::filesystem::path* outDst =
nullptr,
178 std::string* outError =
nullptr)
184 *outError =
"User cancelled";
193 std::vector<std::filesystem::path> out;
198 IFileOpenDialog* pDlg =
nullptr;
199 if (FAILED(CoCreateInstance(CLSID_FileOpenDialog,
nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pDlg))) || !pDlg)
202 COMDLG_FILTERSPEC filters[] = {
203 {L
"Images (*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.webp;*.tga)", L
"*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.webp;*.tga"},
204 {L
"All files (*.*)", L
"*.*"}};
205 pDlg->SetFileTypes(2, filters);
206 pDlg->SetFileTypeIndex(1);
208 DWORD opts = FOS_PATHMUSTEXIST | FOS_FILEMUSTEXIST | FOS_ALLOWMULTISELECT;
209 pDlg->SetOptions(opts);
211 if (SUCCEEDED(pDlg->Show(
nullptr)))
213 IShellItemArray* pArray =
nullptr;
214 if (SUCCEEDED(pDlg->GetResults(&pArray)) && pArray)
217 pArray->GetCount(&count);
218 for (DWORD i = 0; i < count; ++i)
220 IShellItem* pItem =
nullptr;
221 if (SUCCEEDED(pArray->GetItemAt(i, &pItem)) && pItem)
224 if (SUCCEEDED(pItem->GetDisplayName(SIGDN_FILESYSPATH, &psz)) && psz)
226 out.emplace_back(psz);
241 std::vector<std::filesystem::path>* outDsts =
nullptr,
242 std::string* outError =
nullptr)
248 *outError =
"User cancelled";
252 size_t ok = 0, fail = 0;
253 std::vector<std::filesystem::path> dsts_local;
254 for (
auto& f : files)
256 std::filesystem::path dst;
262 outDsts->push_back(dst);
264 dsts_local.push_back(dst);
275 *outError =
"All imports failed";
278 if (fail > 0 && outError)
280 *outError =
"Imported " + std::to_string(ok) +
" file(s), " + std::to_string(fail) +
" failed";
286 std::filesystem::path* outDst =
nullptr,
287 std::string* outError =
nullptr)
290 std::wstring fnameW = L
"asset";
291 auto slash = urlW.find_last_of(L
"/\\");
292 if (slash != std::wstring::npos && slash + 1 < urlW.size())
294 fnameW = urlW.substr(slash + 1);
299 std::string fnameUtf8(fnameW.begin(), fnameW.end());
300 auto ext = std::filesystem::path(fnameUtf8).extension().string();
301 auto stem = std::filesystem::path(fnameUtf8).stem().string();
302 std::string base =
slugify(stem) + ext;
306 std::filesystem::create_directories(destRoot, ec);
310 std::wstring dstW = dst.wstring();
312 HRESULT hr = URLDownloadToFileW(
nullptr, urlW.c_str(), dstW.c_str(), 0,
nullptr);
316 *outError =
"Download failed (HRESULT " + std::to_string(hr) +
")";
327 std::vector<std::filesystem::path> v;
330 if (!std::filesystem::exists(root))
332 for (
auto& e : std::filesystem::directory_iterator(root, ec))
336 if (e.is_regular_file())
337 v.push_back(e.path());
339 std::sort(v.begin(), v.end());
344 std::string* outError =
nullptr)
349 auto root = std::filesystem::weakly_canonical(
assetsDir(kind), ec);
350 if (ec || root.empty() || !std::filesystem::exists(root) || !std::filesystem::is_directory(root))
353 *outError =
"Invalid assets root";
358 auto target = std::filesystem::weakly_canonical(file, ec);
359 if (ec || target.empty())
362 *outError =
"Path canonicalization failed";
368 auto rel = target.lexically_relative(root);
369 if (rel.empty() || rel.native().starts_with(L
"..") || rel.is_absolute())
372 *outError =
"Refusing to delete outside assets dir";
377 if (!std::filesystem::exists(target))
380 *outError =
"File not found";
383 if (!std::filesystem::is_regular_file(target))
386 *outError =
"Not a file";
391 std::filesystem::remove(target, ec);
395 *outError =
"Delete failed: " + ec.message();
403 ImGui::SetNextWindowSize(ImVec2(640, 480), ImGuiCond_Appearing);
404 ImGui::SetNextWindowSizeConstraints(ImVec2(400, 300), ImVec2(FLT_MAX, FLT_MAX));
405 if (ImGui::BeginPopupModal(
"DeleteAssets",
nullptr))
408 ImGui::RadioButton(
"Markers", &tab, 0);
410 ImGui::RadioButton(
"Maps", &tab, 1);
415 ImGui::BeginChild(
"assets_scroll", ImVec2(500, 300),
true);
416 bool deletedThisFrame =
false;
417 for (
auto& p : assets)
419 auto fname = p.filename().string();
420 ImGui::TextUnformatted(fname.c_str());
422 ImGui::PushID(fname.c_str());
423 if (ImGui::SmallButton((
"Delete##" + fname).c_str()))
428 std::cerr <<
"Delete failed: " << err <<
"\n";
429 if (
auto t = toaster_.lock(); t)
434 deletedThisFrame =
true;
435 if (
auto t = toaster_.lock(); t)
440 if (deletedThisFrame)
445 if (ImGui::Button(
"Close"))
446 ImGui::CloseCurrentPopup();
454 if (ImGui::BeginPopupModal(
"UrlAssets",
nullptr, ImGuiWindowFlags_AlwaysAutoResize))
457 ImGui::RadioButton(
"Markers", &tab, 0);
459 ImGui::RadioButton(
"Maps", &tab, 1);
462 auto url_color = (kind ==
AssetKind::Marker) ? ImVec4(0.60f, 0.80f, 1.00f, 1.0f) : ImVec4(0.45f, 0.95f, 0.55f, 1.0f) ;
463 std::wstring url = L
"https://example.com/some.png";
464 std::filesystem::path dst =
assetsDir(kind);
466 ImGui::PushStyleColor(ImGuiCol_Text, url_color);
468 ImGui::PopStyleColor();
471 std::cerr <<
"Import URL failed: " << err <<
"\n";
472 if (
auto t = toaster_.lock(); t)
477 if (
auto t = toaster_.lock(); t)
482 if (ImGui::Button(
"Close"))
483 ImGui::CloseCurrentPopup();
static fs::path getMarkersPath()
static fs::path getMapsPath()
bool importFromPicker(AssetKind kind, std::filesystem::path *outDst=nullptr, std::string *outError=nullptr)
bool deleteAsset(AssetKind kind, const std::filesystem::path &file, std::string *outError=nullptr)
bool importFromPath(AssetKind kind, const std::filesystem::path &srcPath, std::filesystem::path *outDst=nullptr, std::string *outError=nullptr)
std::vector< std::filesystem::path > pickImageFilesWin32()
bool importFromUrl(AssetKind kind, const std::wstring &urlW, std::filesystem::path *outDst=nullptr, std::string *outError=nullptr)
std::optional< std::filesystem::path > pickImageFileWin32()
std::vector< std::filesystem::path > listAssets(AssetKind kind)
std::filesystem::path uniqueName(const std::filesystem::path &destDir, const std::string &baseFile)
const std::filesystem::path & assetsDir(AssetKind kind)
void openUrlAssetPopUp(std::weak_ptr< ImGuiToaster > toaster_)
void openDeleteAssetPopUp(std::weak_ptr< ImGuiToaster > toaster_)
std::string slugify(const std::string &s)
bool importManyFromPicker(AssetKind kind, std::vector< std::filesystem::path > *outDsts=nullptr, std::string *outError=nullptr)