media:cpp : ui 1/2
This commit is contained in:
parent
f18128f4d2
commit
8ead230cc2
@ -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()
|
||||
|
||||
BIN
packages/media/cpp/dist/media-img.exe
vendored
BIN
packages/media/cpp/dist/media-img.exe
vendored
Binary file not shown.
BIN
packages/media/cpp/dist/media-img.pdb
vendored
BIN
packages/media/cpp/dist/media-img.pdb
vendored
Binary file not shown.
@ -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"
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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"};
|
||||
};
|
||||
|
||||
164
packages/media/cpp/src/win/resize_progress_ui.cpp
Normal file
164
packages/media/cpp/src/win/resize_progress_ui.cpp
Normal 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
|
||||
13
packages/media/cpp/src/win/resize_progress_ui.hpp
Normal file
13
packages/media/cpp/src/win/resize_progress_ui.hpp
Normal 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
|
||||
@ -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 (1–100):", WS_CHILD | WS_VISIBLE, x0, y + 2, lw + 48, 20, hwnd,
|
||||
CreateWindowExW(0, L"STATIC", L"Quality (1–100):", 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;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user