media:cpp : ui 1/2

This commit is contained in:
lovebird 2026-04-14 00:03:23 +02:00
parent f18128f4d2
commit 8ead230cc2
15 changed files with 540 additions and 107 deletions

View File

@ -153,6 +153,7 @@ if(WIN32)
target_sources(media-img PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src/win/register_explorer.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/win/resize_ui.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/win/resize_progress_ui.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/win/media-img-win.manifest"
)
endif()

Binary file not shown.

Binary file not shown.

View File

@ -176,6 +176,7 @@ Section "Install"
SetOutPath "$INSTDIR\scripts"
File "..\scripts\explorer-resize.ps1"
File "..\scripts\explorer-convert.ps1"
File "..\scripts\explorer-ui.ps1"
SetOutPath "$INSTDIR"
WriteUninstaller "$INSTDIR\Uninstall.exe"
@ -230,6 +231,7 @@ Section "Uninstall"
RMDir /r "$INSTDIR\vips-modules-8.18"
Delete "$INSTDIR\scripts\explorer-resize.ps1"
Delete "$INSTDIR\scripts\explorer-convert.ps1"
Delete "$INSTDIR\scripts\explorer-ui.ps1"
RMDir "$INSTDIR\scripts"
Delete "$INSTDIR\Uninstall.exe"
RMDir "$INSTDIR"

View File

@ -5,6 +5,7 @@
#include <glob/glob.h>
#include <algorithm>
#include <cctype>
#include <unordered_set>
namespace fs = std::filesystem;
@ -17,6 +18,13 @@ bool trailing_sep(const std::string &s) {
return !s.empty() && (s.back() == '/' || s.back() == '\\');
}
static void trim_in_place(std::string &s) {
while (!s.empty() && std::isspace(static_cast<unsigned char>(s.front())))
s.erase(0, 1);
while (!s.empty() && std::isspace(static_cast<unsigned char>(s.back())))
s.pop_back();
}
void replace_all(std::string &s, const std::string &from, const std::string &to) {
if (from.empty())
return;
@ -75,6 +83,46 @@ std::vector<std::string> expand_input_paths(const std::string &input_spec, std::
if (is_http_url(input_spec))
return {input_spec};
/** Windows Explorer multi-select: semicolon-separated absolute paths ( ';' is invalid in filenames ). */
if (input_spec.find(';') != std::string::npos && !has_glob_tokens(input_spec)) {
std::vector<std::string> parts;
std::string cur;
for (char c : input_spec) {
if (c == ';') {
trim_in_place(cur);
if (!cur.empty())
parts.push_back(std::move(cur));
cur.clear();
} else {
cur.push_back(c);
}
}
trim_in_place(cur);
if (!cur.empty())
parts.push_back(std::move(cur));
if (parts.size() < 2) {
err_out = "invalid semicolon-separated input list";
return {};
}
std::vector<std::string> out;
out.reserve(parts.size());
for (const auto &p : parts) {
fs::path fp(p);
std::error_code ec;
if (!fs::is_regular_file(fp, ec) || ec) {
err_out = "not a file: " + p;
return {};
}
fs::path canon = fs::weakly_canonical(fs::absolute(fp), ec);
if (ec)
canon = fs::absolute(fp);
out.push_back(canon.generic_string());
}
std::sort(out.begin(), out.end());
out.erase(std::unique(out.begin(), out.end()), out.end());
return out;
}
if (!has_glob_tokens(input_spec) && input_spec.find("**") == std::string::npos) {
fs::path p = fs::absolute(fs::path(input_spec));
std::error_code ec;

View File

@ -62,7 +62,8 @@ std::string utf8_truncate_255_bytes(std::string s) {
return s;
}
std::string derive_basename_before_sanitize(const std::string &in, const std::string &format_cli) {
std::string derive_basename_before_sanitize(const std::string &in, const std::string &format_cli,
const std::string &stem_suffix) {
const std::string ext_fmt = ext_for_format(format_cli);
if (is_http_url(in)) {
@ -70,17 +71,27 @@ std::string derive_basename_before_sanitize(const std::string &in, const std::st
fs::path bp(fn);
std::string stem = bp.stem().string();
std::string ext = bp.extension().string();
if (!stem_suffix.empty())
stem += stem_suffix;
if (!format_cli.empty())
return stem + "." + ext_fmt;
if (ext.empty())
return stem + ".jpg";
return fn;
if (stem_suffix.empty())
return fn;
return stem + ext;
}
fs::path p(in);
std::string stem = p.stem().string();
std::string ext = p.extension().string();
if (!stem_suffix.empty())
stem += stem_suffix;
if (!format_cli.empty())
return p.stem().string() + "." + ext_fmt;
return p.filename().string();
return stem + "." + ext_fmt;
if (stem_suffix.empty())
return p.filename().string();
return stem + ext;
}
} // namespace
@ -139,7 +150,7 @@ std::string sanitize_filename(std::string input) {
}
std::string default_output_path_for_resize(const std::string &input_spec, const std::string &format_cli,
std::string &err_out) {
std::string &err_out, const std::string &stem_suffix) {
err_out.clear();
std::string expand_err;
std::vector<std::string> inputs = expand_input_paths(input_spec, expand_err);
@ -156,7 +167,7 @@ std::string default_output_path_for_resize(const std::string &input_spec, const
return {};
}
std::string base = derive_basename_before_sanitize(inputs[0], format_cli);
std::string base = derive_basename_before_sanitize(inputs[0], format_cli, stem_suffix);
std::string safe = sanitize_filename(base);
if (safe.empty()) {
const std::string ext = format_cli.empty() ? std::string("jpg") : ext_for_format(format_cli);
@ -166,12 +177,17 @@ std::string default_output_path_for_resize(const std::string &input_spec, const
}
std::error_code ec;
fs::path cwd = fs::current_path(ec);
if (ec) {
err_out = "resize: cannot get current directory: " + ec.message();
return {};
fs::path out_dir;
if (is_http_url(inputs[0])) {
out_dir = fs::current_path(ec);
if (ec) {
err_out = "resize: cannot get current directory: " + ec.message();
return {};
}
} else {
out_dir = fs::path(inputs[0]).parent_path();
}
return (cwd / safe).lexically_normal().string();
return (out_dir / safe).lexically_normal().string();
}
} // namespace media

View File

@ -12,11 +12,12 @@ std::string sanitize_filename(std::string input);
/**
* When CLI omits output: resolve exactly one input (URL or path / glob one file) and build
* `<cwd>/<sanitized basename>`. `format_cli` empty means infer extension from input; for URLs
* without an extension, default is `.jpg`.
* `<input_parent>/<sanitized basename>` for local files (HTTP(S) inputs still use cwd). `format_cli`
* empty means infer extension from input; for URLs without an extension, default is `.jpg`.
* `stem_suffix` is appended to the stem before the extension (e.g. "_resized").
* @return empty and set err_out on failure (e.g. glob matches multiple files).
*/
std::string default_output_path_for_resize(const std::string &input_spec, const std::string &format_cli,
std::string &err_out);
std::string &err_out, const std::string &stem_suffix = {});
} // namespace media

View File

@ -7,7 +7,9 @@
#include <vips/vips.h>
#include <algorithm>
#include <cstddef>
#include <filesystem>
#include <functional>
#include <mutex>
#include <random>
#include <string>
@ -186,7 +188,8 @@ fs::path unique_temp_download_path(std::string &err_out) {
} // namespace
bool resize_batch(const std::string &input_spec, const std::string &output_spec, const ResizeOptions &opt,
std::string &err_out, ResizeBatchResult *out_stats) {
std::string &err_out, ResizeBatchResult *out_stats,
const std::function<void(std::size_t, std::size_t)> &progress) {
if (out_stats) {
out_stats->count = 0;
out_stats->outputs.clear();
@ -201,7 +204,11 @@ bool resize_batch(const std::string &input_spec, const std::string &output_spec,
err_out = "no resize jobs";
return false;
}
for (const auto &job : jobs) {
const std::size_t n = jobs.size();
for (std::size_t i = 0; i < n; ++i) {
const auto &job = jobs[i];
if (progress)
progress(i, n);
std::error_code ec;
std::filesystem::create_directories(job.second.parent_path(), ec);
std::string one_err;

View File

@ -1,5 +1,7 @@
#pragma once
#include <cstddef>
#include <functional>
#include <string>
#include <vector>
@ -59,6 +61,12 @@ struct ResizeOptions {
int url_timeout_sec = 5;
/** Max redirects when fetching URL inputs. Default 20. */
int url_max_redirects = 20;
/**
* When the resize dialog (or future callers) omit an explicit output path, append this to the stem
* before the extension (e.g. "_resized"). Used only by default_output_path_for_resize; empty = unchanged name.
*/
std::string output_stem_suffix;
};
/** Defaults for `serve` / `ipc` when JSON omits `cache` / `cache_dir`. */
@ -79,9 +87,11 @@ struct ResizeBatchResult {
/**
* One or more inputs (glob `*` `?` `**` or a single file) paired to output file(s) or a directory.
* Stops on first failure; `err_out` names the failing input when possible.
* @param progress If set, invoked before each file with (0-based index, total count).
*/
bool resize_batch(const std::string& input_spec, const std::string& output_spec, const ResizeOptions& opt,
std::string& err_out, ResizeBatchResult* out_stats = nullptr);
std::string& err_out, ResizeBatchResult* out_stats = nullptr,
const std::function<void(std::size_t, std::size_t)>& progress = {});
/** Merge JSON keys into `opt` (REST / IPC). Unknown keys ignored. */
void apply_resize_options_from_json(const nlohmann::json& j, ResizeOptions& opt);

View File

@ -3,6 +3,18 @@
#include <string>
#include <vector>
namespace {
std::string join_src_semicolons(const std::vector<std::string> &v) {
std::string s;
for (size_t i = 0; i < v.size(); ++i) {
if (i)
s += ';';
s += v[i];
}
return s;
}
} // namespace
#include <laserpants/dotenv/dotenv.h>
#include <CLI/CLI.hpp>
@ -18,7 +30,9 @@
#include "http/serve.hpp"
#include "ipc/ipc_serve.hpp"
#if defined(_WIN32)
#include "core/glob_paths.hpp"
#include "win/register_explorer.hpp"
#include "win/resize_progress_ui.hpp"
#include "win/resize_ui.hpp"
#endif
@ -64,7 +78,7 @@ int main(int argc, char **argv) {
std::string in_path;
std::string out_path;
std::string src_flag;
std::vector<std::string> src_list;
std::string dst_flag;
int max_w = 0;
int max_h = 0;
@ -87,7 +101,8 @@ int main(int argc, char **argv) {
resize_cmd->add_option(
"output", out_path,
"Output file/dir, or omit when there is exactly one input → write under cwd (sanitized name)");
resize_cmd->add_option("--src", src_flag, "Same as positional input; glob batch with --dst");
resize_cmd->add_option("--src", src_list, "Input (repeat for multiple); use with --dst; Explorer passes several files")
->expected(-1);
resize_cmd->add_option("--dst", dst_flag, "Same as positional output; directory if multiple inputs");
resize_cmd->add_option("--max-width", max_w, "Target / max width (0 = no limit)");
resize_cmd->add_option("--max-height", max_h, "Target / max height (0 = no limit)");
@ -122,7 +137,7 @@ int main(int argc, char **argv) {
#if defined(_WIN32)
bool resize_ui = false;
resize_cmd->add_flag("--ui", resize_ui,
"Windows: show native dialog to pick input/output and resize options (cannot be used with --src/--dst)");
"Windows: native dialog for paths/options; optional --src/--dst seed the dialog");
#endif
std::string host = "127.0.0.1";
@ -159,6 +174,7 @@ int main(int argc, char **argv) {
std::string reg_media_bin;
std::string reg_explorer_script;
std::string reg_explorer_convert_script;
std::string reg_explorer_ui_script;
std::string reg_widths = "1980,1200,800,400";
auto *reg_cmd = app.add_subcommand(
"register-explorer",
@ -172,6 +188,8 @@ int main(int argc, char **argv) {
"Path to explorer-resize.ps1 (default: <exe_dir>\\scripts\\explorer-resize.ps1)");
reg_cmd->add_option("--explorer-convert-script", reg_explorer_convert_script,
"Path to explorer-convert.ps1 (default: <exe_dir>\\scripts\\explorer-convert.ps1)");
reg_cmd->add_option("--explorer-ui-script", reg_explorer_ui_script,
"Path to explorer-ui.ps1 (default: <exe_dir>\\scripts\\explorer-ui.ps1)");
reg_cmd->add_option("--widths", reg_widths)->default_val("1980,1200,800,400");
CLI11_PARSE(app, argc, argv);
@ -183,8 +201,8 @@ int main(int argc, char **argv) {
#if defined(_WIN32)
if (resize_ui) {
if (!src_flag.empty() || !dst_flag.empty()) {
std::cerr << "resize: --ui cannot be used with --src or --dst\n";
if (!src_list.empty() && !in_path.empty()) {
std::cerr << "resize: use either positional input or --src, not both\n";
return 1;
}
media::ResizeOptions initial{};
@ -208,8 +226,8 @@ int main(int argc, char **argv) {
initial.url_timeout_sec = url_timeout_sec;
initial.url_max_redirects = url_max_redirects;
std::string ui_in = in_path;
std::string ui_out = out_path;
std::string ui_in = src_list.empty() ? in_path : join_src_semicolons(src_list);
std::string ui_out = dst_flag.empty() ? out_path : dst_flag;
if (!media::win::show_resize_ui(opt, ui_in, ui_out, initial))
return 0;
in_path_e = std::move(ui_in);
@ -217,20 +235,20 @@ int main(int argc, char **argv) {
} else
#endif
{
const bool use_src_dst = !src_flag.empty() || !dst_flag.empty();
const bool use_src_dst = !src_list.empty() || !dst_flag.empty();
if (use_src_dst) {
if (src_flag.empty() || dst_flag.empty()) {
std::cerr << "resize: --src and --dst must be used together\n";
if (src_list.empty() || dst_flag.empty()) {
std::cerr << "resize: --src (one or more) and --dst must be used together\n";
return 1;
}
in_path_e = src_flag;
in_path_e = join_src_semicolons(src_list);
out_path_e = dst_flag;
} else if (in_path.empty()) {
std::cerr << "resize: provide input (and output, or omit output to write under the current directory)\n";
return 1;
} else if (out_path.empty()) {
std::string derr;
out_path_e = media::default_output_path_for_resize(in_path, format, derr);
out_path_e = media::default_output_path_for_resize(in_path, format, derr, {});
if (out_path_e.empty()) {
std::cerr << derr << "\n";
return 1;
@ -264,7 +282,7 @@ int main(int argc, char **argv) {
if (out_path_e.empty()) {
std::string derr;
out_path_e = media::default_output_path_for_resize(in_path_e, opt.format, derr);
out_path_e = media::default_output_path_for_resize(in_path_e, opt.format, derr, opt.output_stem_suffix);
if (out_path_e.empty()) {
std::cerr << derr << "\n";
return 1;
@ -273,7 +291,24 @@ int main(int argc, char **argv) {
std::string err;
media::ResizeBatchResult batch;
if (!media::resize_batch(in_path_e, out_path_e, opt, err, &batch)) {
#if defined(_WIN32)
bool batch_ok;
{
std::string preview_err;
auto jobs_preview = media::pair_resize_paths(in_path_e, out_path_e, preview_err);
if (!preview_err.empty()) {
std::cerr << preview_err << "\n";
return 1;
}
if (jobs_preview.size() > 1)
batch_ok = media::win::run_resize_batch_with_progress_ui(in_path_e, out_path_e, opt, err, &batch);
else
batch_ok = media::resize_batch(in_path_e, out_path_e, opt, err, &batch);
}
#else
const bool batch_ok = media::resize_batch(in_path_e, out_path_e, opt, err, &batch);
#endif
if (!batch_ok) {
std::cerr << err << "\n";
return 1;
}
@ -321,6 +356,7 @@ int main(int argc, char **argv) {
o.media_bin = reg_media_bin;
o.explorer_script = reg_explorer_script;
o.explorer_convert_script = reg_explorer_convert_script;
o.explorer_ui_script = reg_explorer_ui_script;
o.widths = reg_widths;
return media::win::register_explorer_run(o);
#else

View File

@ -90,6 +90,10 @@ std::wstring default_explorer_convert_script(const std::wstring &exe_dir) {
return default_script_path(exe_dir, L"explorer-convert.ps1");
}
std::wstring default_explorer_ui_script(const std::wstring &exe_dir) {
return default_script_path(exe_dir, L"explorer-ui.ps1");
}
bool parse_widths(const std::string &s, std::vector<int> &out) {
out.clear();
std::string cur;
@ -144,7 +148,8 @@ std::vector<std::wstring> ext_dot_variants(const std::string &canon_lower_ascii)
struct AssocTarget {
std::wstring classes_suffix;
std::wstring path_token;
/** `Directory\\Background` uses `%V`; file/folder targets use `%1`. */
bool is_background = false;
};
LONG create_key(HKEY root, const std::wstring &rel, HKEY *out) {
@ -164,20 +169,30 @@ bool set_default_sz(HKEY key, const std::wstring &value) {
return RegSetValueExW(key, nullptr, 0, REG_SZ, data, cb) == ERROR_SUCCESS;
}
std::wstring build_resize_command_line(const std::wstring &script, const std::wstring &media,
const std::wstring &path_token, int w, bool inplace) {
std::wstring cmd =
L"powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \"" + script +
L"\" -MediaImg \"" + media + L"\" -Path \"" + path_token + L"\" -MaxWidth " + std::to_wstring(w) +
L" -Mode ";
/** Single path per invocation (Document model — default; works with cascaded `subCommands`). `%*` is not expanded. */
static const wchar_t k_explorer_path_one[] = L" \"%1\"";
/** Folder background: working directory under cursor. */
static const wchar_t k_explorer_background_folder[] = L" \"%V\"";
std::wstring build_resize_command_line(const std::wstring &script, const std::wstring &media, int w, bool inplace,
const std::wstring &path_suffix) {
std::wstring cmd = L"powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \"" + script +
L"\" -MediaImg \"" + media + L"\" -MaxWidth " + std::to_wstring(w) + L" -Mode ";
cmd += inplace ? L"InPlace" : L"Copy";
cmd += path_suffix;
return cmd;
}
std::wstring build_convert_command_line(const std::wstring &convert_script, const std::wstring &media,
const std::wstring &path_token) {
const std::wstring &path_suffix) {
return L"powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \"" + convert_script +
L"\" -MediaImg \"" + media + L"\" -Path \"" + path_token + L"\"";
L"\" -MediaImg \"" + media + L"\"" + path_suffix;
}
std::wstring build_resize_ui_command_line(const std::wstring &ui_script, const std::wstring &media,
const std::wstring &path_suffix) {
return L"powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \"" + ui_script +
L"\" -MediaImg \"" + media + L"\"" + path_suffix;
}
bool write_verbed_command(HKEY root, const std::wstring &shell_group, const std::wstring &id,
@ -201,9 +216,10 @@ bool write_verbed_command(HKEY root, const std::wstring &shell_group, const std:
return ok;
}
bool register_one_target(HKEY root, const std::wstring &classes_suffix, const std::wstring &path_token,
const std::wstring &group, const std::wstring &resize_script,
const std::wstring &convert_script, const std::wstring &media, const std::vector<int> &widths) {
bool register_one_target(HKEY root, const std::wstring &classes_suffix, const std::wstring &group,
const std::wstring &resize_script, const std::wstring &convert_script,
const std::wstring &ui_script, const std::wstring &media, const std::vector<int> &widths,
const std::wstring &path_suffix) {
const std::wstring base = std::wstring(L"Software\\Classes\\") + classes_suffix;
const std::wstring shell_group = base + L"\\shell\\" + group;
@ -223,7 +239,7 @@ bool register_one_target(HKEY root, const std::wstring &classes_suffix, const st
auto write_resize = [&](const std::wstring &id, const std::wstring &mui_name, bool inplace) -> bool {
return write_verbed_command(root, shell_group, id, mui_name,
build_resize_command_line(resize_script, media, path_token, w, inplace));
build_resize_command_line(resize_script, media, w, inplace, path_suffix));
};
if (!write_resize(id_in, name_in, true))
@ -232,7 +248,11 @@ bool register_one_target(HKEY root, const std::wstring &classes_suffix, const st
return false;
}
const std::wstring cvt_line = build_convert_command_line(convert_script, media, path_token);
const std::wstring ui_line = build_resize_ui_command_line(ui_script, media, path_suffix);
if (!write_verbed_command(root, shell_group, L"rszopt", L"Resize with options…", ui_line))
return false;
const std::wstring cvt_line = build_convert_command_line(convert_script, media, path_suffix);
if (!write_verbed_command(root, shell_group, L"cvtjpg", L"Convert to JPG", cvt_line))
return false;
@ -257,12 +277,11 @@ std::vector<AssocTarget> build_file_association_targets() {
for (const std::wstring &dot : ext_dot_variants(canon)) {
AssocTarget t;
t.classes_suffix = L"SystemFileAssociations\\" + dot;
t.path_token = L"%1";
out.push_back(std::move(t));
}
}
out.push_back(AssocTarget{L"Directory", L"%1"});
out.push_back(AssocTarget{L"Directory\\Background", L"%V"});
out.push_back(AssocTarget{L"Directory", false});
out.push_back(AssocTarget{L"Directory\\Background", true});
return out;
}
@ -280,6 +299,8 @@ int register_explorer_run(const RegisterExplorerOptions &opt) {
std::wstring convert_script_w = opt.explorer_convert_script.empty()
? default_explorer_convert_script(exe_dir)
: utf8_to_wide(opt.explorer_convert_script);
std::wstring ui_script_w =
opt.explorer_ui_script.empty() ? default_explorer_ui_script(exe_dir) : utf8_to_wide(opt.explorer_ui_script);
std::vector<int> widths;
if (!parse_widths(opt.widths, widths)) {
@ -300,6 +321,10 @@ int register_explorer_run(const RegisterExplorerOptions &opt) {
std::cerr << "register-explorer: convert script not found: " << wide_to_utf8(convert_script_w) << "\n";
return 1;
}
if (!file_exists_w(ui_script_w)) {
std::cerr << "register-explorer: UI script not found: " << wide_to_utf8(ui_script_w) << "\n";
return 1;
}
}
const std::wstring group_w = utf8_to_wide(opt.group);
@ -341,16 +366,22 @@ int register_explorer_run(const RegisterExplorerOptions &opt) {
}
if (opt.dry) {
std::cout << "Dry run (sample for %1):\n";
const std::wstring sample_tok = L"%1";
std::cout << "Dry run (per-file `%1`; multi-select runs once per file; folder background uses `%V`):\n";
for (int w : widths) {
std::cout << " " << wide_to_utf8(build_resize_command_line(resize_script_w, media_w, sample_tok, w, true))
std::cout << " "
<< wide_to_utf8(
build_resize_command_line(resize_script_w, media_w, w, true, k_explorer_path_one))
<< "\n";
std::cout << " " << wide_to_utf8(build_resize_command_line(resize_script_w, media_w, sample_tok, w, false))
std::cout << " "
<< wide_to_utf8(
build_resize_command_line(resize_script_w, media_w, w, false, k_explorer_path_one))
<< "\n";
}
std::cout << " " << wide_to_utf8(build_convert_command_line(convert_script_w, media_w, sample_tok)) << "\n";
std::cout << "Also register folder background with %V.\n";
std::cout << " " << wide_to_utf8(build_convert_command_line(convert_script_w, media_w, k_explorer_path_one))
<< "\n";
std::cout << " " << wide_to_utf8(build_resize_ui_command_line(ui_script_w, media_w, k_explorer_path_one))
<< "\n";
std::cout << " (under folder background: suffix uses %V)\n";
std::cout << "File types: " << (sizeof(k_canonical_ext) / sizeof(k_canonical_ext[0]))
<< " canonical extensions × 3 case variants under SystemFileAssociations.\n";
return 0;
@ -359,8 +390,9 @@ int register_explorer_run(const RegisterExplorerOptions &opt) {
const std::vector<AssocTarget> targets = build_file_association_targets();
for (const auto &t : targets) {
if (!register_one_target(HKEY_CURRENT_USER, t.classes_suffix, t.path_token, group_w, resize_script_w,
convert_script_w, media_w, widths)) {
const std::wstring &suffix = t.is_background ? k_explorer_background_folder : k_explorer_path_one;
if (!register_one_target(HKEY_CURRENT_USER, t.classes_suffix, group_w, resize_script_w, convert_script_w,
ui_script_w, media_w, widths, suffix)) {
std::cerr << "register-explorer: failed to register a context-menu target\n";
return 1;
}

View File

@ -15,6 +15,8 @@ struct RegisterExplorerOptions {
std::string explorer_script;
/** Empty = <exe_dir>\\scripts\\explorer-convert.ps1 */
std::string explorer_convert_script;
/** Empty = <exe_dir>\\scripts\\explorer-ui.ps1 (multi-select → one UI) */
std::string explorer_ui_script;
/** Comma-separated positive integers */
std::string widths{"1980,1200,800,400"};
};

View File

@ -0,0 +1,164 @@
#include "resize_progress_ui.hpp"
#include "core/glob_paths.hpp"
#include "core/resize.hpp"
#include <windows.h>
#include <commctrl.h>
#include <string>
#include <thread>
#pragma comment(lib, "Comctl32.lib")
#pragma comment(lib, "User32.lib")
namespace media::win {
namespace {
enum : int {
IDC_PROG_BAR = 200,
IDC_PROG_TEXT = 201,
};
enum : UINT { WM_PI_PROG = WM_USER + 70, WM_PI_DONE = WM_USER + 71 };
struct ProgCtx {
HWND root{};
HWND bar{};
HWND text{};
};
static LRESULT CALLBACK ProgWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
auto *ctx = reinterpret_cast<ProgCtx *>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
switch (msg) {
case WM_CREATE:
return 0;
case WM_PI_PROG: {
if (!ctx || !ctx->bar || !ctx->text)
return 0;
const std::size_t one_based = static_cast<std::size_t>(wp);
const std::size_t total = static_cast<std::size_t>(lp);
if (total > 0) {
SendMessageW(ctx->bar, PBM_SETRANGE32, 0, static_cast<LPARAM>(total - 1));
SendMessageW(ctx->bar, PBM_SETPOS, static_cast<WPARAM>(one_based - 1), 0);
}
wchar_t line[256]{};
swprintf_s(line, L"Processing %zu of %zu…", one_based, total);
SetWindowTextW(ctx->text, line);
return 0;
}
case WM_PI_DONE:
PostQuitMessage(0);
return 0;
case WM_CLOSE:
DestroyWindow(hwnd);
return 0;
case WM_DESTROY:
return 0;
default:
return DefWindowProcW(hwnd, msg, wp, lp);
}
}
static const wchar_t kProgClass[] = L"MediaImgResizeProgress";
static bool register_prog_class() {
static bool done = false;
if (done)
return true;
WNDCLASSEXW wc{};
wc.cbSize = sizeof(wc);
wc.lpfnWndProc = ProgWndProc;
wc.hInstance = GetModuleHandleW(nullptr);
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_3DFACE + 1);
wc.lpszClassName = kProgClass;
if (!RegisterClassExW(&wc)) {
if (GetLastError() != ERROR_CLASS_ALREADY_EXISTS)
return false;
}
done = true;
return true;
}
} // namespace
bool run_resize_batch_with_progress_ui(const std::string &input_spec, const std::string &output_spec,
const ResizeOptions &opt, std::string &err_out, ResizeBatchResult *out_stats) {
std::string pair_err;
auto jobs = media::pair_resize_paths(input_spec, output_spec, pair_err);
if (!pair_err.empty()) {
err_out = pair_err;
return false;
}
if (jobs.empty()) {
err_out = "no resize jobs";
return false;
}
if (jobs.size() == 1)
return media::resize_batch(input_spec, output_spec, opt, err_out, out_stats);
INITCOMMONCONTROLSEX icc{};
icc.dwSize = sizeof(icc);
icc.dwICC = ICC_PROGRESS_CLASS;
InitCommonControlsEx(&icc);
if (!register_prog_class())
return media::resize_batch(input_spec, output_spec, opt, err_out, out_stats);
ProgCtx ctx{};
const int PW = 420;
const int PH = 120;
RECT r{};
SystemParametersInfoW(SPI_GETWORKAREA, 0, &r, 0);
const int sx = r.left + ((r.right - r.left) - PW) / 2;
const int sy = r.top + ((r.bottom - r.top) - PH) / 2;
HWND hwnd = CreateWindowExW(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE, kProgClass, L"media-img — resizing",
WS_CAPTION | WS_SYSMENU | WS_CLIPCHILDREN, sx, sy, PW, PH, nullptr, nullptr,
GetModuleHandleW(nullptr), nullptr);
if (!hwnd)
return media::resize_batch(input_spec, output_spec, opt, err_out, out_stats);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(&ctx));
ctx.root = hwnd;
const int pad = 16;
ctx.text = CreateWindowExW(0, L"STATIC", L"Starting…", WS_CHILD | WS_VISIBLE, pad, pad, PW - pad * 2, 22, hwnd,
(HMENU)IDC_PROG_TEXT, nullptr, nullptr);
ctx.bar = CreateWindowExW(0, PROGRESS_CLASSW, L"", WS_CHILD | WS_VISIBLE | PBS_SMOOTH, pad, pad + 28, PW - pad * 2,
24, hwnd, (HMENU)IDC_PROG_BAR, nullptr, nullptr);
SendMessageW(ctx.bar, PBM_SETRANGE32, 0, static_cast<LPARAM>(jobs.size() - 1));
bool ok = false;
std::string worker_err;
std::thread worker([&]() {
ok = media::resize_batch(
input_spec, output_spec, opt, worker_err, out_stats,
[&](std::size_t i, std::size_t total) {
PostMessageW(hwnd, WM_PI_PROG, static_cast<WPARAM>(i + 1), static_cast<LPARAM>(total));
});
PostMessageW(hwnd, WM_PI_DONE, 0, 0);
});
ShowWindow(hwnd, SW_SHOW);
UpdateWindow(hwnd);
MSG msg;
while (GetMessageW(&msg, nullptr, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
worker.join();
if (IsWindow(hwnd))
DestroyWindow(hwnd);
if (!ok)
err_out = std::move(worker_err);
return ok;
}
} // namespace media::win

View File

@ -0,0 +1,13 @@
#pragma once
#include "core/resize.hpp"
#include <string>
namespace media::win {
/** Windows: run `resize_batch` with a native progress bar when multiple files; single job uses `resize_batch` only. */
bool run_resize_batch_with_progress_ui(const std::string &input_spec, const std::string &output_spec,
const ResizeOptions &opt, std::string &err_out, ResizeBatchResult *out_stats);
} // namespace media::win

View File

@ -56,6 +56,8 @@ enum : int {
IDC_CHK_ENLARGE = 114,
IDC_CHK_AUTOROT = 115,
IDC_CHK_STRIP = 116,
IDC_STATIC_PRESET = 117,
IDC_COMBO_OUT_PRESET = 118,
IDC_BTN_OK = IDOK,
IDC_BTN_CANCEL = IDCANCEL,
};
@ -88,6 +90,8 @@ struct UiState {
HWND root{};
HWND h_in{};
HWND h_out{};
HWND h_btn_in{};
HWND h_btn_out{};
HWND h_mw{};
HWND h_mh{};
HWND h_fit{};
@ -95,12 +99,38 @@ struct UiState {
HWND h_enlarge{};
HWND h_autorot{};
HWND h_strip{};
HWND h_out_preset{};
int y_preset{};
int x0{16};
int lw{108};
int gap{8};
int btn_w{90};
int margin_r{14};
int y_in{};
int y_out{};
ResizeOptions *opt{};
std::string *in_path{};
std::string *out_path{};
bool accepted{false};
};
static void layout_stretch_path_rows(UiState *st, int client_w) {
if (!st || !st->h_in || !st->h_out || client_w < 320)
return;
const int edit_x = st->x0 + st->lw + st->gap;
const int browse_x = client_w - st->margin_r - st->btn_w;
if (browse_x < edit_x + 80)
return;
const int ew = browse_x - st->gap - edit_x;
const int eh = 26;
MoveWindow(st->h_in, edit_x, st->y_in, ew, eh, TRUE);
MoveWindow(st->h_btn_in, browse_x, st->y_in, st->btn_w, 28, TRUE);
if (st->h_out_preset)
MoveWindow(st->h_out_preset, edit_x, st->y_preset, ew, 180, TRUE);
MoveWindow(st->h_out, edit_x, st->y_out, ew, eh, TRUE);
MoveWindow(st->h_btn_out, browse_x, st->y_out, st->btn_w, 28, TRUE);
}
static void set_utf8_edit(HWND h, const std::string &utf8) {
SetWindowTextW(h, utf8_to_wide(utf8).c_str());
}
@ -120,8 +150,9 @@ static void browse_open(HWND owner, HWND h_edit) {
OPENFILENAMEW of{};
of.lStructSize = sizeof(of);
of.hwndOwner = owner;
of.lpstrFilter = L"Images\0*.jpg;*.jpeg;*.png;*.webp;*.tif;*.tiff;*.bmp;*.gif;*.avif;*.heic;*.jpe;*.jfif\0"
L"All\0*.*\0\0";
of.lpstrFilter =
L"Images\0*.jpg;*.jpeg;*.png;*.webp;*.tif;*.tiff;*.bmp;*.gif;*.avif;*.heic;*.jpe;*.jfif;*.jf\0"
L"All\0*.*\0\0";
of.nFilterIndex = 1;
of.lpstrFile = buf;
of.nMaxFile = MAX_PATH * 4;
@ -153,61 +184,89 @@ static LRESULT CALLBACK UiWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
st = reinterpret_cast<UiState *>(cs->lpCreateParams);
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(st));
st->root = hwnd;
const int pad = 12;
const int x0 = 20;
int y = pad + 6;
const int lw = 128;
const int gap = 10;
const int ew = 340;
const int btn_w = 96;
const int btn_h = 28;
const int eh = 24;
st->x0 = 14;
st->lw = 108;
st->gap = 8;
st->btn_w = 90;
st->margin_r = 14;
const int x0 = st->x0;
const int lw = st->lw;
const int gap = st->gap;
const int btn_w = st->btn_w;
const int margin_r = st->margin_r;
int y = 10;
const int ew = 300;
const int btn_h = 26;
const int eh = 22;
const int edit_x = x0 + lw + gap;
const int browse_x = edit_x + ew + gap;
CreateWindowExW(0, L"STATIC", L"Input image:", WS_CHILD | WS_VISIBLE, x0, y + 2, lw, 20, hwnd,
CreateWindowExW(0, L"STATIC", L"Input image:", WS_CHILD | WS_VISIBLE, x0, y + 1, lw, 18, hwnd,
(HMENU)IDC_STATIC_IN, nullptr, nullptr);
st->y_in = y;
st->h_in = CreateWindowExW(
WS_EX_CLIENTEDGE, L"EDIT", L"",
WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, ew, eh + 4, hwnd, (HMENU)IDC_EDIT_IN, nullptr,
WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, ew, eh + 2, hwnd, (HMENU)IDC_EDIT_IN, nullptr,
nullptr);
CreateWindowExW(0, L"BUTTON", L"Browse…", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, browse_x, y, btn_w, btn_h,
hwnd, (HMENU)IDC_BTN_IN, nullptr, nullptr);
y += 44;
st->h_btn_in = CreateWindowExW(0, L"BUTTON", L"Browse…", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, browse_x, y,
btn_w, btn_h, hwnd, (HMENU)IDC_BTN_IN, nullptr, nullptr);
y += 34;
CreateWindowExW(0, L"STATIC", L"Output (optional):", WS_CHILD | WS_VISIBLE, x0, y + 2, lw, 20, hwnd,
st->y_preset = y;
CreateWindowExW(0, L"STATIC", L"Output preset:", WS_CHILD | WS_VISIBLE, x0, y + 1, lw, 18, hwnd,
(HMENU)IDC_STATIC_PRESET, nullptr, nullptr);
st->h_out_preset = CreateWindowExW(
WS_EX_CLIENTEDGE, L"COMBOBOX", L"",
WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | CBS_HASSTRINGS, edit_x, y - 1, ew, 180, hwnd,
(HMENU)IDC_COMBO_OUT_PRESET, nullptr, nullptr);
{
const wchar_t *presets[] = {L"Next to source (default name)", L"Next to source (stem_resized)",
L"Use path below (custom)"};
for (const wchar_t *t : presets)
SendMessageW(st->h_out_preset, CB_ADDSTRING, 0, reinterpret_cast<LPARAM>(t));
int pr = 0;
if (!st->out_path->empty())
pr = 2;
else if (st->opt->output_stem_suffix == "_resized")
pr = 1;
SendMessageW(st->h_out_preset, CB_SETCURSEL, static_cast<WPARAM>(pr), 0);
}
y += 34;
CreateWindowExW(0, L"STATIC", L"Custom path (optional):", WS_CHILD | WS_VISIBLE, x0, y + 1, lw + 20, 18, hwnd,
(HMENU)IDC_STATIC_OUT, nullptr, nullptr);
st->y_out = y;
st->h_out = CreateWindowExW(
WS_EX_CLIENTEDGE, L"EDIT", L"",
WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, ew, eh + 4, hwnd, (HMENU)IDC_EDIT_OUT,
WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, ew, eh + 2, hwnd, (HMENU)IDC_EDIT_OUT,
nullptr, nullptr);
CreateWindowExW(0, L"BUTTON", L"Browse…", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, browse_x, y, btn_w, btn_h,
hwnd, (HMENU)IDC_BTN_OUT, nullptr, nullptr);
y += 48;
st->h_btn_out = CreateWindowExW(0, L"BUTTON", L"Browse…", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, browse_x, y,
btn_w, btn_h, hwnd, (HMENU)IDC_BTN_OUT, nullptr, nullptr);
y += 34;
CreateWindowExW(0, L"STATIC", L"Max width:", WS_CHILD | WS_VISIBLE, x0, y + 2, lw, 20, hwnd,
CreateWindowExW(0, L"STATIC", L"Max width:", WS_CHILD | WS_VISIBLE, x0, y + 1, lw, 18, hwnd,
(HMENU)IDC_STATIC_MW, nullptr, nullptr);
wchar_t mw[32]{};
swprintf_s(mw, L"%d", st->opt->max_width);
st->h_mw = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", mw,
WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, 108, eh + 4, hwnd,
WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, 96, eh + 2, hwnd,
(HMENU)IDC_EDIT_MW, nullptr, nullptr);
const int mh_label_x = edit_x + 108 + gap + 24;
CreateWindowExW(0, L"STATIC", L"Max height:", WS_CHILD | WS_VISIBLE, mh_label_x, y + 2, 100, 20, hwnd,
const int mh_label_x = edit_x + 96 + gap + 16;
CreateWindowExW(0, L"STATIC", L"Max height:", WS_CHILD | WS_VISIBLE, mh_label_x, y + 1, 88, 18, hwnd,
(HMENU)IDC_STATIC_MH, nullptr, nullptr);
wchar_t mh[32]{};
swprintf_s(mh, L"%d", st->opt->max_height);
st->h_mh = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", mh,
WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, mh_label_x + 100 + gap, y, 108, eh + 4, hwnd,
WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, mh_label_x + 88 + gap, y, 96, eh + 2, hwnd,
(HMENU)IDC_EDIT_MH, nullptr, nullptr);
y += 44;
y += 34;
CreateWindowExW(0, L"STATIC", L"Fit:", WS_CHILD | WS_VISIBLE, x0, y + 2, lw, 20, hwnd, (HMENU)IDC_STATIC_FIT,
CreateWindowExW(0, L"STATIC", L"Fit:", WS_CHILD | WS_VISIBLE, x0, y + 1, lw, 18, hwnd, (HMENU)IDC_STATIC_FIT,
nullptr, nullptr);
st->h_fit = CreateWindowExW(WS_EX_CLIENTEDGE, L"COMBOBOX", L"",
WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | CBS_HASSTRINGS, edit_x, y - 2, 240,
200, hwnd, (HMENU)IDC_COMBO_FIT, nullptr, nullptr);
WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | CBS_HASSTRINGS, edit_x, y - 1, 220,
180, hwnd, (HMENU)IDC_COMBO_FIT, nullptr, nullptr);
const wchar_t *fits[] = {L"inside", L"cover", L"contain", L"fill", L"outside"};
for (const wchar_t *f : fits)
SendMessageW(st->h_fit, CB_ADDSTRING, 0, reinterpret_cast<LPARAM>(f));
@ -226,41 +285,46 @@ static LRESULT CALLBACK UiWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
}
SendMessageW(st->h_fit, CB_SETCURSEL, static_cast<WPARAM>(sel), 0);
}
y += 40;
y += 32;
CreateWindowExW(0, L"STATIC", L"Quality (1100):", WS_CHILD | WS_VISIBLE, x0, y + 2, lw + 48, 20, hwnd,
CreateWindowExW(0, L"STATIC", L"Quality (1100):", WS_CHILD | WS_VISIBLE, x0, y + 1, lw + 40, 18, hwnd,
(HMENU)IDC_STATIC_Q, nullptr, nullptr);
wchar_t q[16]{};
swprintf_s(q, L"%d", st->opt->quality);
st->h_q = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", q,
WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, 72, eh + 4, hwnd,
WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, edit_x, y, 64, eh + 2, hwnd,
(HMENU)IDC_EDIT_Q, nullptr, nullptr);
y += 44;
y += 32;
st->h_enlarge =
CreateWindowExW(0, L"BUTTON", L"Allow enlargement", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, x0, y, 220,
26, hwnd, (HMENU)IDC_CHK_ENLARGE, nullptr, nullptr);
CreateWindowExW(0, L"BUTTON", L"Allow enlargement", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, x0, y, 210,
22, hwnd, (HMENU)IDC_CHK_ENLARGE, nullptr, nullptr);
SendMessageW(st->h_enlarge, BM_SETCHECK,
st->opt->without_enlargement ? BST_UNCHECKED : BST_CHECKED, 0);
st->h_autorot =
CreateWindowExW(0, L"BUTTON", L"EXIF autorotate", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, x0 + 240, y, 200,
26, hwnd, (HMENU)IDC_CHK_AUTOROT, nullptr, nullptr);
CreateWindowExW(0, L"BUTTON", L"EXIF autorotate", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, x0 + 228, y, 188,
22, hwnd, (HMENU)IDC_CHK_AUTOROT, nullptr, nullptr);
SendMessageW(st->h_autorot, BM_SETCHECK, st->opt->autorotate ? BST_CHECKED : BST_UNCHECKED, 0);
y += 34;
y += 26;
st->h_strip = CreateWindowExW(0, L"BUTTON", L"Strip metadata on save", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX,
x0, y, 280, 26, hwnd, (HMENU)IDC_CHK_STRIP, nullptr, nullptr);
x0, y, 268, 22, hwnd, (HMENU)IDC_CHK_STRIP, nullptr, nullptr);
SendMessageW(st->h_strip, BM_SETCHECK, st->opt->strip_metadata ? BST_CHECKED : BST_UNCHECKED, 0);
y += 42;
y += 34;
const int btn_row_w = 100;
const int btn_row_w = 92;
CreateWindowExW(0, L"BUTTON", L"OK", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, browse_x - btn_row_w - gap - btn_row_w,
y, btn_row_w, 32, hwnd, (HMENU)IDC_BTN_OK, nullptr, nullptr);
y, btn_row_w, 28, hwnd, (HMENU)IDC_BTN_OK, nullptr, nullptr);
CreateWindowExW(0, L"BUTTON", L"Cancel", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, browse_x - btn_row_w, y, btn_row_w,
32, hwnd, (HMENU)IDC_BTN_CANCEL, nullptr, nullptr);
28, hwnd, (HMENU)IDC_BTN_CANCEL, nullptr, nullptr);
set_utf8_edit(st->h_in, *st->in_path);
set_utf8_edit(st->h_out, *st->out_path);
apply_message_font(hwnd);
{
RECT cr{};
GetClientRect(hwnd, &cr);
layout_stretch_path_rows(st, cr.right);
}
return 0;
}
case WM_COMMAND: {
@ -286,7 +350,18 @@ static LRESULT CALLBACK UiWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
MessageBoxW(hwnd, L"Choose an input image.", L"media-img", MB_OK | MB_ICONWARNING);
return 0;
}
*st->out_path = get_utf8_edit(st->h_out);
{
const int ps = static_cast<int>(SendMessageW(st->h_out_preset, CB_GETCURSEL, 0, 0));
st->opt->output_stem_suffix.clear();
if (ps == 0) {
*st->out_path = {};
} else if (ps == 1) {
*st->out_path = {};
st->opt->output_stem_suffix = "_resized";
} else {
*st->out_path = get_utf8_edit(st->h_out);
}
}
wchar_t bmw[32]{};
GetWindowTextW(st->h_mw, bmw, 32);
@ -323,6 +398,33 @@ static LRESULT CALLBACK UiWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
}
return 0;
}
case WM_CTLCOLORSTATIC: {
HDC hdc = reinterpret_cast<HDC>(wp);
SetTextColor(hdc, GetSysColor(COLOR_WINDOWTEXT));
SetBkColor(hdc, GetSysColor(COLOR_3DFACE));
return reinterpret_cast<LRESULT>(GetSysColorBrush(COLOR_3DFACE));
}
case WM_CTLCOLOREDIT:
case WM_CTLCOLORLISTBOX: {
HDC hdc = reinterpret_cast<HDC>(wp);
SetTextColor(hdc, GetSysColor(COLOR_WINDOWTEXT));
SetBkColor(hdc, GetSysColor(COLOR_WINDOW));
return reinterpret_cast<LRESULT>(GetSysColorBrush(COLOR_WINDOW));
}
case WM_SIZE: {
if (st && (wp == SIZE_RESTORED || wp == SIZE_MAXIMIZED)) {
RECT cr{};
GetClientRect(hwnd, &cr);
layout_stretch_path_rows(st, cr.right);
}
return DefWindowProcW(hwnd, msg, wp, lp);
}
case WM_GETMINMAXINFO: {
auto *mmi = reinterpret_cast<MINMAXINFO *>(lp);
mmi->ptMinTrackSize.x = 460;
mmi->ptMinTrackSize.y = 300;
return 0;
}
case WM_CLOSE:
if (st)
st->accepted = false;
@ -367,17 +469,16 @@ bool show_resize_ui(ResizeOptions &opt, std::string &input_path, std::string &ou
state.in_path = &input_path;
state.out_path = &output_path;
const int W = 620;
const int H = 452;
const int W = 520;
const int H = 430;
RECT r{};
SystemParametersInfoW(SPI_GETWORKAREA, 0, &r, 0);
const int sx = r.left + ((r.right - r.left) - W) / 2;
const int sy = r.top + ((r.bottom - r.top) - H) / 2;
HWND hwnd =
CreateWindowExW(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE, kClassName, L"media-img — resize",
WS_CAPTION | WS_SYSMENU | WS_CLIPCHILDREN, sx, sy, W, H, nullptr, nullptr, GetModuleHandleW(nullptr),
&state);
const DWORD style = (WS_OVERLAPPEDWINDOW & ~WS_MAXIMIZEBOX) | WS_CLIPCHILDREN;
HWND hwnd = CreateWindowExW(WS_EX_WINDOWEDGE, kClassName, L"media-img — resize", style, sx, sy, W, H, nullptr,
nullptr, GetModuleHandleW(nullptr), &state);
if (!hwnd)
return false;