Add NightShot source
This commit is contained in:
+23
@@ -0,0 +1,23 @@
|
||||
DEV_README.md
|
||||
THIRD_PARTY_NOTICES.md
|
||||
|
||||
dist/
|
||||
build/
|
||||
|
||||
*.o
|
||||
*.obj
|
||||
*.exe
|
||||
*.res
|
||||
*.dll
|
||||
*.pdb
|
||||
*.ilk
|
||||
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
*.zip
|
||||
*.sha256
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
@@ -1,3 +1,33 @@
|
||||
# NightShot
|
||||
|
||||
NightShot — быстрый нативный Win64-инструмент для создания скриншотов и записи GIF. Выделение области, аннотации, ColorPicker, загрузка на свой API-сервер, системные уведомления, мультимониторный режим и автосохранение.
|
||||
NightShot — нативное приложение для Windows 10/11, предназначенное для быстрого создания и редактирования скриншотов, а также записи GIF с выбранной области экрана.
|
||||
|
||||
Приложение написано на C++20 с использованием WinAPI и GDI+, не требует CMake, сторонних UI-фреймворков или установки дополнительных runtime-компонентов.
|
||||
|
||||
## Возможности
|
||||
|
||||
- Захват произвольной области экрана через `PrintScreen`
|
||||
- Поддержка нескольких мониторов и разных значений DPI
|
||||
- Перемещение и изменение размера выделенной области
|
||||
- Карандаш, линии, стрелки, прямоугольники, маркер и текст
|
||||
- Настройка толщины инструментов колёсиком мыши
|
||||
- ColorPicker с пипеткой, HEX, RGBA и HSV
|
||||
- Сохраняемая палитра цветов 8×6
|
||||
- Копирование изображения в буфер обмена
|
||||
- Сохранение снимков без перезаписи существующих файлов
|
||||
- Запись GIF через `Ctrl + PrintScreen`
|
||||
- Выбор FPS, качества, курсора и лимита записи
|
||||
- Автоматическое сохранение GIF и помещение результата в буфер
|
||||
- Загрузка изображений на пользовательский API-сервер
|
||||
- Системные уведомления Windows
|
||||
- Иконка в трее и окно настроек
|
||||
- Автозагрузка и запуск с правами администратора
|
||||
- Полностью нативная Win64-сборка
|
||||
|
||||
## Сборка
|
||||
|
||||
Для сборки требуется MinGW-w64 с поддержкой C++20.
|
||||
|
||||
```bat
|
||||
build.bat
|
||||
```
|
||||
@@ -0,0 +1,139 @@
|
||||
@echo off
|
||||
setlocal EnableExtensions EnableDelayedExpansion
|
||||
cd /d "%~dp0"
|
||||
title NightShot Win64 build
|
||||
|
||||
set "EXITCODE=0"
|
||||
set "PAUSE_ON_EXIT=1"
|
||||
if /I "%~1"=="--no-pause" set "PAUSE_ON_EXIT=0"
|
||||
set "CXX_PATH="
|
||||
set "WINDRES_PATH="
|
||||
set "RES_OBJECT="
|
||||
|
||||
rem Prefer an explicitly named MinGW-w64 compiler, then a 64-bit g++ from PATH.
|
||||
for /f "delims=" %%P in ('where x86_64-w64-mingw32-g++.exe 2^>nul') do if not defined CXX_PATH set "CXX_PATH=%%P"
|
||||
if not defined CXX_PATH for /f "delims=" %%P in ('where g++.exe 2^>nul') do if not defined CXX_PATH set "CXX_PATH=%%P"
|
||||
|
||||
if not defined CXX_PATH (
|
||||
echo [ERROR] A Win64 MinGW-w64 g++ compiler was not found.
|
||||
echo Install MSYS2 UCRT64/MinGW64 or add its bin directory to PATH.
|
||||
set "EXITCODE=1"
|
||||
goto :finish
|
||||
)
|
||||
|
||||
for /f "delims=" %%T in ('""!CXX_PATH!" -dumpmachine 2^>nul"') do set "TARGET=%%T"
|
||||
echo !TARGET! | findstr /I /C:"x86_64-w64-mingw32" >nul
|
||||
if errorlevel 1 (
|
||||
echo [ERROR] The detected compiler is not a Win64 MinGW-w64 compiler.
|
||||
echo Compiler: !CXX_PATH!
|
||||
echo Target: !TARGET!
|
||||
set "EXITCODE=1"
|
||||
goto :finish
|
||||
)
|
||||
|
||||
rem windres is often next to g++.exe but is not always added to PATH.
|
||||
for %%P in ("!CXX_PATH!") do set "TOOLBIN=%%~dpP"
|
||||
for %%R in ("!TOOLBIN!x86_64-w64-mingw32-windres.exe" "!TOOLBIN!!TARGET!-windres.exe" "!TOOLBIN!windres.exe") do (
|
||||
if not defined WINDRES_PATH if exist "%%~fR" set "WINDRES_PATH=%%~fR"
|
||||
)
|
||||
if not defined WINDRES_PATH for /f "delims=" %%P in ('where x86_64-w64-mingw32-windres.exe 2^>nul') do if not defined WINDRES_PATH set "WINDRES_PATH=%%P"
|
||||
if not defined WINDRES_PATH for /f "delims=" %%P in ('where windres.exe 2^>nul') do if not defined WINDRES_PATH set "WINDRES_PATH=%%P"
|
||||
|
||||
if not exist build mkdir build
|
||||
if not exist dist mkdir dist
|
||||
|
||||
set "COMMON=-std=c++20 -O2 -fms-extensions -Wall -Wextra -Wpedantic -DUNICODE -D_UNICODE -D_WIN32_WINNT=0x0A00 -D_WIN32_IE=0x0600 -finput-charset=UTF-8 -Iinclude -ffunction-sections -fdata-sections"
|
||||
|
||||
echo Compiler: !CXX_PATH!
|
||||
echo Target: !TARGET!
|
||||
echo.
|
||||
|
||||
echo [1/13] Compiling capture.cpp
|
||||
"!CXX_PATH!" !COMMON! -c src\capture.cpp -o build\capture.o
|
||||
if errorlevel 1 goto :build_failed
|
||||
|
||||
echo [2/13] Compiling image_io.cpp
|
||||
"!CXX_PATH!" !COMMON! -c src\image_io.cpp -o build\image_io.o
|
||||
if errorlevel 1 goto :build_failed
|
||||
|
||||
echo [3/13] Compiling gif_recorder.cpp
|
||||
"!CXX_PATH!" !COMMON! -c src\gif_recorder.cpp -o build\gif_recorder.o
|
||||
if errorlevel 1 goto :build_failed
|
||||
|
||||
echo [4/13] Compiling gif_options_window.cpp
|
||||
"!CXX_PATH!" !COMMON! -c src\gif_options_window.cpp -o build\gif_options_window.o
|
||||
if errorlevel 1 goto :build_failed
|
||||
|
||||
echo [5/13] Compiling overlay.cpp
|
||||
"!CXX_PATH!" !COMMON! -c src\overlay.cpp -o build\overlay.o
|
||||
if errorlevel 1 goto :build_failed
|
||||
|
||||
echo [6/13] Compiling notifications.cpp
|
||||
"!CXX_PATH!" !COMMON! -c src\notifications.cpp -o build\notifications.o
|
||||
if errorlevel 1 goto :build_failed
|
||||
|
||||
echo [7/13] Compiling settings.cpp
|
||||
"!CXX_PATH!" !COMMON! -c src\settings.cpp -o build\settings.o
|
||||
if errorlevel 1 goto :build_failed
|
||||
|
||||
echo [8/13] Compiling startup.cpp
|
||||
"!CXX_PATH!" !COMMON! -c src\startup.cpp -o build\startup.o
|
||||
if errorlevel 1 goto :build_failed
|
||||
|
||||
echo [9/13] Compiling settings_window.cpp
|
||||
"!CXX_PATH!" !COMMON! -c src\settings_window.cpp -o build\settings_window.o
|
||||
if errorlevel 1 goto :build_failed
|
||||
|
||||
echo [10/13] Compiling uploader.cpp
|
||||
"!CXX_PATH!" !COMMON! -c src\uploader.cpp -o build\uploader.o
|
||||
if errorlevel 1 goto :build_failed
|
||||
|
||||
echo [11/13] Compiling main.cpp
|
||||
"!CXX_PATH!" !COMMON! -c src\main.cpp -o build\main.o
|
||||
if errorlevel 1 goto :build_failed
|
||||
|
||||
if defined WINDRES_PATH (
|
||||
echo [12/13] Compiling resources
|
||||
"!WINDRES_PATH!" -Iinclude -Ires res\app.rc -O coff -o build\app_res.o
|
||||
if errorlevel 1 goto :build_failed
|
||||
set "RES_OBJECT=build\app_res.o"
|
||||
) else (
|
||||
echo [12/13] Resources skipped
|
||||
echo [WARN] Win64 windres was not found. The EXE will still build, but without the embedded icon/version/manifest.
|
||||
echo [WARN] Install the full MSYS2 mingw-w64-x86_64-binutils package to restore resources.
|
||||
)
|
||||
|
||||
echo [13/13] Linking Win64 executable
|
||||
"!CXX_PATH!" -municode -mwindows -static -static-libgcc -static-libstdc++ ^
|
||||
-Wl,--gc-sections -Wl,--nxcompat -Wl,--dynamicbase -Wl,--high-entropy-va ^
|
||||
build\capture.o build\image_io.o build\gif_recorder.o build\gif_options_window.o ^
|
||||
build\overlay.o build\notifications.o ^
|
||||
build\settings.o build\startup.o build\settings_window.o build\uploader.o ^
|
||||
build\main.o !RES_OBJECT! ^
|
||||
-lgdiplus -lgdi32 -luser32 -lshell32 -lshcore -lole32 -lruntimeobject ^
|
||||
-lcomdlg32 -luuid -lwinhttp -ladvapi32 -lwindowscodecs ^
|
||||
-o dist\NightShot.exe
|
||||
if errorlevel 1 goto :build_failed
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
|
||||
"$b=[IO.File]::ReadAllBytes('dist\NightShot.exe');$pe=[BitConverter]::ToInt32($b,0x3c);$magic=[BitConverter]::ToUInt16($b,$pe+24);if($magic-ne 0x20b){Write-Error 'Output is not PE32+ Win64';exit 1}"
|
||||
if errorlevel 1 goto :build_failed
|
||||
|
||||
echo.
|
||||
echo [OK] Built: dist\NightShot.exe [Win64 / PE32+]
|
||||
set "EXITCODE=0"
|
||||
goto :finish
|
||||
|
||||
:build_failed
|
||||
set "EXITCODE=!ERRORLEVEL!"
|
||||
if "!EXITCODE!"=="0" set "EXITCODE=1"
|
||||
echo.
|
||||
echo [ERROR] Build failed with exit code !EXITCODE!.
|
||||
|
||||
:finish
|
||||
echo.
|
||||
if "!PAUSE_ON_EXIT!"=="1" (
|
||||
echo Press any key to close this window...
|
||||
pause >nul
|
||||
)
|
||||
exit /b !EXITCODE!
|
||||
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
cd /d "%~dp0"
|
||||
if exist build rmdir /s /q build
|
||||
if exist dist rmdir /s /q dist
|
||||
echo Cleaned.
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
inline constexpr UINT WM_NIGHTSHOT_SCREENSHOT_COPIED = WM_APP + 12;
|
||||
inline constexpr UINT WM_NIGHTSHOT_OPEN_SETTINGS = WM_APP + 13;
|
||||
inline constexpr UINT WM_NIGHTSHOT_SETTINGS_CHANGED = WM_APP + 14;
|
||||
inline constexpr UINT WM_NIGHTSHOT_UPLOAD_FINISHED = WM_APP + 15;
|
||||
inline constexpr UINT WM_NIGHTSHOT_BEGIN_GIF = WM_APP + 16;
|
||||
inline constexpr UINT WM_NIGHTSHOT_GIF_FINISHED = WM_APP + 17;
|
||||
inline constexpr UINT WM_NIGHTSHOT_SCREENSHOT_SAVED = WM_APP + 18;
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
#include "config.hpp"
|
||||
#include <windows.h>
|
||||
|
||||
struct ScreenCapture {
|
||||
HDC memoryDc = nullptr;
|
||||
HBITMAP bitmap = nullptr;
|
||||
HGDIOBJ oldBitmap = nullptr;
|
||||
void* pixels = nullptr;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int stride = 0;
|
||||
|
||||
ScreenCapture() = default;
|
||||
ScreenCapture(const ScreenCapture&) = delete;
|
||||
ScreenCapture& operator=(const ScreenCapture&) = delete;
|
||||
ScreenCapture(ScreenCapture&& other) noexcept;
|
||||
ScreenCapture& operator=(ScreenCapture&& other) noexcept;
|
||||
~ScreenCapture();
|
||||
|
||||
[[nodiscard]] bool valid() const noexcept { return memoryDc && bitmap && width > 0 && height > 0; }
|
||||
void reset() noexcept;
|
||||
};
|
||||
|
||||
[[nodiscard]] ScreenCapture captureVirtualDesktop();
|
||||
@@ -0,0 +1,119 @@
|
||||
#pragma once
|
||||
|
||||
#include "config.hpp"
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cwctype>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace color_picker_math {
|
||||
|
||||
struct Hsv {
|
||||
float h = 0.0f; // [0, 360)
|
||||
float s = 0.0f; // [0, 1]
|
||||
float v = 0.0f; // [0, 1]
|
||||
};
|
||||
|
||||
inline Hsv rgbToHsv(BYTE red, BYTE green, BYTE blue) {
|
||||
const float r = static_cast<float>(red) / 255.0f;
|
||||
const float g = static_cast<float>(green) / 255.0f;
|
||||
const float b = static_cast<float>(blue) / 255.0f;
|
||||
const float maximum = std::max({r, g, b});
|
||||
const float minimum = std::min({r, g, b});
|
||||
const float delta = maximum - minimum;
|
||||
|
||||
Hsv result;
|
||||
result.v = maximum;
|
||||
result.s = maximum <= 0.0f ? 0.0f : delta / maximum;
|
||||
if (delta <= 0.00001f) {
|
||||
result.h = 0.0f;
|
||||
} else if (maximum == r) {
|
||||
result.h = 60.0f * std::fmod((g - b) / delta, 6.0f);
|
||||
} else if (maximum == g) {
|
||||
result.h = 60.0f * (((b - r) / delta) + 2.0f);
|
||||
} else {
|
||||
result.h = 60.0f * (((r - g) / delta) + 4.0f);
|
||||
}
|
||||
if (result.h < 0.0f) result.h += 360.0f;
|
||||
if (result.h >= 360.0f) result.h = std::fmod(result.h, 360.0f);
|
||||
return result;
|
||||
}
|
||||
|
||||
inline COLORREF hsvToRgb(float hue, float saturation, float value) {
|
||||
hue = std::fmod(hue, 360.0f);
|
||||
if (hue < 0.0f) hue += 360.0f;
|
||||
saturation = std::clamp(saturation, 0.0f, 1.0f);
|
||||
value = std::clamp(value, 0.0f, 1.0f);
|
||||
|
||||
const float chroma = value * saturation;
|
||||
const float section = hue / 60.0f;
|
||||
const float x = chroma * (1.0f - std::fabs(std::fmod(section, 2.0f) - 1.0f));
|
||||
const float m = value - chroma;
|
||||
|
||||
float r = 0.0f;
|
||||
float g = 0.0f;
|
||||
float b = 0.0f;
|
||||
if (section < 1.0f) { r = chroma; g = x; }
|
||||
else if (section < 2.0f) { r = x; g = chroma; }
|
||||
else if (section < 3.0f) { g = chroma; b = x; }
|
||||
else if (section < 4.0f) { g = x; b = chroma; }
|
||||
else if (section < 5.0f) { r = x; b = chroma; }
|
||||
else { r = chroma; b = x; }
|
||||
|
||||
const auto channel = [m](float component) -> BYTE {
|
||||
return static_cast<BYTE>(std::clamp(static_cast<int>(std::lround((component + m) * 255.0f)), 0, 255));
|
||||
};
|
||||
return RGB(channel(r), channel(g), channel(b));
|
||||
}
|
||||
|
||||
inline std::uint32_t toArgb(COLORREF color, BYTE alpha) {
|
||||
return (static_cast<std::uint32_t>(alpha) << 24u) |
|
||||
(static_cast<std::uint32_t>(GetRValue(color)) << 16u) |
|
||||
(static_cast<std::uint32_t>(GetGValue(color)) << 8u) |
|
||||
static_cast<std::uint32_t>(GetBValue(color));
|
||||
}
|
||||
|
||||
inline COLORREF colorFromArgb(std::uint32_t argb) {
|
||||
return RGB((argb >> 16u) & 0xFFu, (argb >> 8u) & 0xFFu, argb & 0xFFu);
|
||||
}
|
||||
|
||||
inline BYTE alphaFromArgb(std::uint32_t argb) {
|
||||
return static_cast<BYTE>((argb >> 24u) & 0xFFu);
|
||||
}
|
||||
|
||||
inline std::wstring hexRgba(COLORREF color, BYTE alpha) {
|
||||
std::wostringstream stream;
|
||||
stream << L'#' << std::uppercase << std::hex << std::setfill(L'0')
|
||||
<< std::setw(2) << static_cast<unsigned int>(GetRValue(color))
|
||||
<< std::setw(2) << static_cast<unsigned int>(GetGValue(color))
|
||||
<< std::setw(2) << static_cast<unsigned int>(GetBValue(color))
|
||||
<< std::setw(2) << static_cast<unsigned int>(alpha);
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
inline bool parseHexRgba(std::wstring text, COLORREF& color, BYTE& alpha) {
|
||||
if (!text.empty() && text.front() == L'#') text.erase(text.begin());
|
||||
if (text.size() != 6 && text.size() != 8) return false;
|
||||
for (wchar_t ch : text) {
|
||||
if (!std::iswxdigit(static_cast<wint_t>(ch))) return false;
|
||||
}
|
||||
wchar_t* end = nullptr;
|
||||
const unsigned long long value = std::wcstoull(text.c_str(), &end, 16);
|
||||
if (!end || *end != L'\0') return false;
|
||||
if (text.size() == 6) {
|
||||
color = RGB((value >> 16u) & 0xFFu, (value >> 8u) & 0xFFu, value & 0xFFu);
|
||||
alpha = 255;
|
||||
} else {
|
||||
color = RGB((value >> 24u) & 0xFFu, (value >> 16u) & 0xFFu, (value >> 8u) & 0xFFu);
|
||||
alpha = static_cast<BYTE>(value & 0xFFu);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace color_picker_math
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#if !defined(_WIN32)
|
||||
# error "NightShot targets Windows."
|
||||
#endif
|
||||
|
||||
// The distributed build is intentionally Win64-only. Developers may explicitly
|
||||
// opt into a compatibility build for testing, but build.bat never does this.
|
||||
#if !defined(_WIN64) && !defined(NIGHTSHOT_ALLOW_WIN32)
|
||||
# error "NightShot is Win64-only. Define NIGHTSHOT_ALLOW_WIN32 only for unsupported compatibility testing."
|
||||
#endif
|
||||
|
||||
#ifndef UNICODE
|
||||
# define UNICODE
|
||||
#endif
|
||||
#ifndef _UNICODE
|
||||
# define _UNICODE
|
||||
#endif
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "config.hpp"
|
||||
#include "gif_recorder.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace gif_options_window {
|
||||
|
||||
// Native modal Win32 window. Returns true when the user presses Start.
|
||||
[[nodiscard]] bool show(HWND owner, const settings::AppSettings& defaults,
|
||||
gif_recorder::Options& options);
|
||||
|
||||
} // namespace gif_options_window
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include "config.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace gif_recorder {
|
||||
|
||||
struct Options {
|
||||
int fps = 30;
|
||||
int quality = 8;
|
||||
// 0 means manual stop. hardLimitSeconds is always enforced.
|
||||
int durationSeconds = 0;
|
||||
int hardLimitSeconds = 30;
|
||||
bool captureCursor = true;
|
||||
};
|
||||
|
||||
struct StartRequest {
|
||||
RECT screenRect{};
|
||||
Options options{};
|
||||
HWND notificationTarget = nullptr;
|
||||
};
|
||||
|
||||
struct Result {
|
||||
bool success = false;
|
||||
std::wstring path;
|
||||
std::wstring message;
|
||||
int frameCount = 0;
|
||||
int elapsedMilliseconds = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] bool start(const StartRequest& request);
|
||||
void stop();
|
||||
[[nodiscard]] bool active();
|
||||
// Called by the main window after consuming WM_NIGHTSHOT_GIF_FINISHED.
|
||||
void releaseFinished();
|
||||
void shutdown();
|
||||
|
||||
} // namespace gif_recorder
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
#include "config.hpp"
|
||||
#include "settings.hpp"
|
||||
#include <windows.h>
|
||||
#include <objidl.h>
|
||||
#include <gdiplus.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
[[nodiscard]] int findEncoderClsid(const WCHAR* mimeType, CLSID* clsid);
|
||||
[[nodiscard]] bool saveBitmapAsPng(Gdiplus::Bitmap& bitmap, const std::wstring& path);
|
||||
[[nodiscard]] bool saveBitmapAsJpeg(Gdiplus::Bitmap& bitmap, const std::wstring& path, int quality);
|
||||
[[nodiscard]] bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap);
|
||||
[[nodiscard]] bool encodeBitmapAsPng(Gdiplus::Bitmap& bitmap, std::vector<BYTE>& output);
|
||||
|
||||
// Reserves a non-existing incremented file name with CREATE_NEW. The returned
|
||||
// empty file belongs to the caller and may safely be filled/overwritten.
|
||||
[[nodiscard]] std::wstring reserveIncrementedPath(
|
||||
const std::wstring& directory, const wchar_t* prefix,
|
||||
const wchar_t* extension);
|
||||
|
||||
[[nodiscard]] bool saveScreenshotIncremented(
|
||||
Gdiplus::Bitmap& bitmap, const settings::AppSettings& configuration,
|
||||
std::wstring& savedPath);
|
||||
|
||||
enum class PrintBitmapResult { Printed, Cancelled, Failed };
|
||||
[[nodiscard]] PrintBitmapResult printBitmap(HWND owner, Gdiplus::Bitmap& bitmap);
|
||||
[[nodiscard]] std::wstring choosePngPath(HWND owner);
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "config.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace notifications {
|
||||
|
||||
// Registers the portable executable as a Start-menu notification source and
|
||||
// initializes the Windows Runtime notification API. Failure is non-fatal.
|
||||
[[nodiscard]] bool initialize();
|
||||
|
||||
[[nodiscard]] bool show(const wchar_t* title, const wchar_t* body);
|
||||
[[nodiscard]] bool showScreenshotCopied();
|
||||
|
||||
void shutdown();
|
||||
|
||||
} // namespace notifications
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include "capture.hpp"
|
||||
#include "config.hpp"
|
||||
#include <windows.h>
|
||||
|
||||
enum class OverlayMode {
|
||||
Screenshot,
|
||||
GifQuick,
|
||||
};
|
||||
|
||||
class OverlayWindow {
|
||||
public:
|
||||
static HWND create(HINSTANCE instance, ScreenCapture&& capture,
|
||||
HWND notificationTarget,
|
||||
OverlayMode mode = OverlayMode::Screenshot);
|
||||
|
||||
private:
|
||||
OverlayWindow(ScreenCapture&& capture, HWND notificationTarget,
|
||||
OverlayMode mode);
|
||||
~OverlayWindow();
|
||||
OverlayWindow(const OverlayWindow&) = delete;
|
||||
OverlayWindow& operator=(const OverlayWindow&) = delete;
|
||||
|
||||
static LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
|
||||
LRESULT handleMessage(UINT message, WPARAM wParam, LPARAM lParam);
|
||||
|
||||
struct Impl;
|
||||
Impl* impl_;
|
||||
int dispatchDepth_ = 0;
|
||||
bool deletePending_ = false;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
#define IDI_APP_ICON 101
|
||||
#define IDR_APP_MANIFEST 1
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include "config.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace settings {
|
||||
|
||||
inline constexpr std::size_t kSavedColorCount = 48;
|
||||
|
||||
enum class ScreenshotFormat : int {
|
||||
Png = 0,
|
||||
Jpeg = 1,
|
||||
};
|
||||
|
||||
struct AppSettings {
|
||||
bool notificationsEnabled = true;
|
||||
bool startWithWindows = true;
|
||||
bool silentAdmin = false;
|
||||
|
||||
bool copyUploadedUrl = true;
|
||||
bool openUploadedUrl = false;
|
||||
bool closeEditorAfterUpload = true;
|
||||
|
||||
std::wstring uploadUrl;
|
||||
std::wstring uploadName = L"Screenshot1";
|
||||
std::wstring multipartField = L"file";
|
||||
|
||||
// Files are saved automatically with non-destructive incrementing names:
|
||||
// Screenshot_N.png/.jpg and GIFShot_N.gif.
|
||||
std::wstring saveDirectory;
|
||||
ScreenshotFormat screenshotFormat = ScreenshotFormat::Png;
|
||||
int jpegQuality = 92;
|
||||
|
||||
int gifFps = 30;
|
||||
int gifQuality = 8;
|
||||
int gifMaxDurationSeconds = 30;
|
||||
bool gifCaptureCursor = true;
|
||||
|
||||
// Drawing widths are physical pixels and intentionally persist immediately
|
||||
// when the mouse wheel changes the active tool.
|
||||
int penWidth = 3;
|
||||
int lineWidth = 3;
|
||||
int arrowWidth = 3;
|
||||
int rectangleWidth = 3;
|
||||
int markerWidth = 15;
|
||||
|
||||
// ARGB values. Empty slots remain available for one-click saving from the
|
||||
// in-editor 8x6 color picker.
|
||||
std::array<std::uint32_t, kSavedColorCount> paletteArgb{};
|
||||
std::array<bool, kSavedColorCount> paletteOccupied{};
|
||||
};
|
||||
|
||||
[[nodiscard]] AppSettings load();
|
||||
[[nodiscard]] bool save(const AppSettings& value);
|
||||
[[nodiscard]] bool exists();
|
||||
[[nodiscard]] std::wstring filePath();
|
||||
[[nodiscard]] std::wstring defaultSaveDirectory();
|
||||
[[nodiscard]] std::wstring normalizeEndpoint(std::wstring value);
|
||||
|
||||
} // namespace settings
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "config.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace settings_window {
|
||||
|
||||
void show(HWND owner);
|
||||
[[nodiscard]] bool handleDialogMessage(MSG* message);
|
||||
[[nodiscard]] HWND window();
|
||||
|
||||
} // namespace settings_window
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "config.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
|
||||
namespace startup {
|
||||
|
||||
[[nodiscard]] bool isProcessElevated();
|
||||
|
||||
// Returns true only in the elevated instance that should continue running.
|
||||
// A non-elevated launch starts one elevated copy through UAC and returns false.
|
||||
// When an existing NightShot window is already running, it returns false without
|
||||
// producing another UAC prompt.
|
||||
[[nodiscard]] bool ensureElevated(const wchar_t* mainWindowClass);
|
||||
|
||||
// Normal mode stores an HKCU Run entry. At logon that starts a tiny unelevated
|
||||
// instance which immediately requests UAC once. Silent mode replaces it with a
|
||||
// highest-privilege Task Scheduler task, so no UAC appears at logon.
|
||||
[[nodiscard]] bool applyAutostart(bool enabled, bool silentAdmin,
|
||||
std::wstring* error = nullptr);
|
||||
|
||||
} // namespace startup
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
|
||||
namespace theme {
|
||||
// Graphite / indigo palette from the supplied native GDI+ UI reference.
|
||||
inline constexpr COLORREF kAccent = RGB(59, 130, 246);
|
||||
inline constexpr COLORREF kAccentEnd = RGB(139, 92, 246);
|
||||
inline constexpr COLORREF kAccentHover = RGB(96, 165, 250);
|
||||
inline constexpr COLORREF kWindow = RGB(18, 18, 24);
|
||||
inline constexpr COLORREF kPanel = RGB(24, 24, 34);
|
||||
inline constexpr COLORREF kPanelTop = RGB(30, 30, 42);
|
||||
inline constexpr COLORREF kPanelBottom = RGB(19, 19, 28);
|
||||
inline constexpr COLORREF kPanelHover = RGB(43, 43, 57);
|
||||
inline constexpr COLORREF kPanelActive = RGB(59, 70, 108);
|
||||
inline constexpr COLORREF kBorder = RGB(60, 60, 80);
|
||||
inline constexpr COLORREF kText = RGB(240, 240, 250);
|
||||
inline constexpr COLORREF kMuted = RGB(150, 150, 170);
|
||||
inline constexpr COLORREF kSelectionBorder = RGB(96, 165, 250);
|
||||
|
||||
// Annotation widths stay in physical pixels. Toolbar metrics are calculated
|
||||
// per monitor from its effective DPI in overlay.cpp.
|
||||
inline constexpr int kStrokeWidth = 3;
|
||||
inline constexpr int kMarkerWidth = 15;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "config.hpp"
|
||||
#include "settings.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace uploader {
|
||||
|
||||
struct Result {
|
||||
bool success = false;
|
||||
DWORD httpStatus = 0;
|
||||
std::wstring responseUrl;
|
||||
std::wstring message;
|
||||
};
|
||||
|
||||
// Result ownership is transferred to the window through
|
||||
// WM_NIGHTSHOT_UPLOAD_FINISHED in lParam.
|
||||
[[nodiscard]] bool start(HWND notificationTarget, std::vector<BYTE>&& png,
|
||||
const settings::AppSettings& configuration);
|
||||
|
||||
} // namespace uploader
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity version="1.0.0.0" processorArchitecture="amd64" name="NightShot" type="win32"/>
|
||||
<description>NightShot Win64 screenshot utility</description>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
<supportedOS Id="{4f476546-937d-4f38-a4b6-5d7fc6f4f253}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
#include "../include/resource.h"
|
||||
IDI_APP_ICON ICON "nightshot.ico"
|
||||
IDR_APP_MANIFEST RT_MANIFEST "app.manifest"
|
||||
|
||||
1 VERSIONINFO
|
||||
FILEVERSION 0,17,0,0
|
||||
PRODUCTVERSION 0,17,0,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
FILEFLAGS 0x0L
|
||||
FILEOS 0x40004L
|
||||
FILETYPE 0x1L
|
||||
FILESUBTYPE 0x0L
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904b0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "NightSync"
|
||||
VALUE "FileDescription", "NightShot Win64 screenshot utility"
|
||||
VALUE "FileVersion", "0.17.0"
|
||||
VALUE "InternalName", "NightShot"
|
||||
VALUE "OriginalFilename", "NightShot.exe"
|
||||
VALUE "ProductName", "NightShot"
|
||||
VALUE "ProductVersion", "0.17.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200
|
||||
END
|
||||
END
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,71 @@
|
||||
#include "capture.hpp"
|
||||
#include <utility>
|
||||
|
||||
ScreenCapture::ScreenCapture(ScreenCapture&& other) noexcept { *this = std::move(other); }
|
||||
|
||||
ScreenCapture& ScreenCapture::operator=(ScreenCapture&& other) noexcept {
|
||||
if (this != &other) {
|
||||
reset();
|
||||
memoryDc = std::exchange(other.memoryDc, nullptr);
|
||||
bitmap = std::exchange(other.bitmap, nullptr);
|
||||
oldBitmap = std::exchange(other.oldBitmap, nullptr);
|
||||
pixels = std::exchange(other.pixels, nullptr);
|
||||
x = std::exchange(other.x, 0);
|
||||
y = std::exchange(other.y, 0);
|
||||
width = std::exchange(other.width, 0);
|
||||
height = std::exchange(other.height, 0);
|
||||
stride = std::exchange(other.stride, 0);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
ScreenCapture::~ScreenCapture() { reset(); }
|
||||
|
||||
void ScreenCapture::reset() noexcept {
|
||||
if (memoryDc && oldBitmap) {
|
||||
SelectObject(memoryDc, oldBitmap);
|
||||
}
|
||||
if (bitmap) DeleteObject(bitmap);
|
||||
if (memoryDc) DeleteDC(memoryDc);
|
||||
memoryDc = nullptr;
|
||||
bitmap = nullptr;
|
||||
oldBitmap = nullptr;
|
||||
pixels = nullptr;
|
||||
x = y = width = height = stride = 0;
|
||||
}
|
||||
|
||||
ScreenCapture captureVirtualDesktop() {
|
||||
ScreenCapture result;
|
||||
result.x = GetSystemMetrics(SM_XVIRTUALSCREEN);
|
||||
result.y = GetSystemMetrics(SM_YVIRTUALSCREEN);
|
||||
result.width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||
result.height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
result.stride = result.width * 4;
|
||||
if (result.width <= 0 || result.height <= 0) return result;
|
||||
|
||||
HDC screenDc = GetDC(nullptr);
|
||||
if (!screenDc) return result;
|
||||
|
||||
result.memoryDc = CreateCompatibleDC(screenDc);
|
||||
BITMAPINFO info{};
|
||||
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
info.bmiHeader.biWidth = result.width;
|
||||
info.bmiHeader.biHeight = -result.height;
|
||||
info.bmiHeader.biPlanes = 1;
|
||||
info.bmiHeader.biBitCount = 32;
|
||||
info.bmiHeader.biCompression = BI_RGB;
|
||||
|
||||
result.bitmap = CreateDIBSection(screenDc, &info, DIB_RGB_COLORS, &result.pixels, nullptr, 0);
|
||||
if (result.memoryDc && result.bitmap) {
|
||||
result.oldBitmap = SelectObject(result.memoryDc, result.bitmap);
|
||||
if (!BitBlt(result.memoryDc, 0, 0, result.width, result.height, screenDc,
|
||||
result.x, result.y, SRCCOPY | CAPTUREBLT)) {
|
||||
result.reset();
|
||||
}
|
||||
} else {
|
||||
result.reset();
|
||||
}
|
||||
|
||||
ReleaseDC(nullptr, screenDc);
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
#include "gif_options_window.hpp"
|
||||
|
||||
#include "resource.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cwchar>
|
||||
#include <iterator>
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
namespace gif_options_window {
|
||||
namespace {
|
||||
constexpr wchar_t kClassName[] = L"NightShotGifOptionsWindow";
|
||||
constexpr int kIdFps = 4101;
|
||||
constexpr int kIdQuality = 4102;
|
||||
constexpr int kIdDuration = 4103;
|
||||
constexpr int kIdCursor = 4104;
|
||||
constexpr int kIdStart = 4105;
|
||||
constexpr int kIdCancel = 4106;
|
||||
|
||||
struct State {
|
||||
HWND owner = nullptr;
|
||||
HWND window = nullptr;
|
||||
HFONT font = nullptr;
|
||||
UINT dpi = 96;
|
||||
bool accepted = false;
|
||||
bool finished = false;
|
||||
gif_recorder::Options options{};
|
||||
|
||||
HWND group = nullptr;
|
||||
HWND fpsLabel = nullptr;
|
||||
HWND fps = nullptr;
|
||||
HWND qualityLabel = nullptr;
|
||||
HWND quality = nullptr;
|
||||
HWND durationLabel = nullptr;
|
||||
HWND duration = nullptr;
|
||||
HWND durationHint = nullptr;
|
||||
HWND cursor = nullptr;
|
||||
HWND start = nullptr;
|
||||
HWND cancel = nullptr;
|
||||
};
|
||||
|
||||
ATOM g_class = 0;
|
||||
|
||||
int px(UINT dpi, int value) {
|
||||
return MulDiv(value, static_cast<int>(dpi), 96);
|
||||
}
|
||||
|
||||
void applyFont(HWND control, HFONT font) {
|
||||
if (control) SendMessageW(control, WM_SETFONT, reinterpret_cast<WPARAM>(font), TRUE);
|
||||
}
|
||||
|
||||
HWND createControl(State& state, const wchar_t* cls, const wchar_t* text,
|
||||
DWORD style, int id, DWORD exStyle = 0) {
|
||||
HWND control = CreateWindowExW(
|
||||
exStyle, cls, text, WS_CHILD | WS_VISIBLE | style,
|
||||
0, 0, 0, 0, state.window,
|
||||
reinterpret_cast<HMENU>(static_cast<INT_PTR>(id)),
|
||||
GetModuleHandleW(nullptr), nullptr);
|
||||
applyFont(control, state.font);
|
||||
return control;
|
||||
}
|
||||
|
||||
void replaceFont(State& state, UINT dpi) {
|
||||
state.dpi = dpi ? dpi : 96;
|
||||
if (state.font) DeleteObject(state.font);
|
||||
state.font = CreateFontW(
|
||||
-MulDiv(9, static_cast<int>(state.dpi), 72), 0, 0, 0, FW_NORMAL,
|
||||
FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS,
|
||||
CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
|
||||
DEFAULT_PITCH | FF_DONTCARE, L"Segoe UI");
|
||||
const std::array<HWND, 12> controls{
|
||||
state.group, state.fpsLabel, state.fps, state.qualityLabel,
|
||||
state.quality, state.durationLabel, state.duration,
|
||||
state.durationHint, state.cursor, state.start, state.cancel,
|
||||
state.window};
|
||||
for (HWND control : controls) applyFont(control, state.font);
|
||||
}
|
||||
|
||||
void addComboValue(HWND combo, const std::wstring& label, int value) {
|
||||
const LRESULT index = SendMessageW(combo, CB_ADDSTRING, 0,
|
||||
reinterpret_cast<LPARAM>(label.c_str()));
|
||||
if (index >= 0) SendMessageW(combo, CB_SETITEMDATA, index, value);
|
||||
}
|
||||
|
||||
void selectComboValue(HWND combo, int value) {
|
||||
const int count = static_cast<int>(SendMessageW(combo, CB_GETCOUNT, 0, 0));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (static_cast<int>(SendMessageW(combo, CB_GETITEMDATA, i, 0)) == value) {
|
||||
SendMessageW(combo, CB_SETCURSEL, i, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
SendMessageW(combo, CB_SETCURSEL, 0, 0);
|
||||
}
|
||||
|
||||
int comboValue(HWND combo, int fallback) {
|
||||
const LRESULT index = SendMessageW(combo, CB_GETCURSEL, 0, 0);
|
||||
if (index == CB_ERR) return fallback;
|
||||
const LRESULT value = SendMessageW(combo, CB_GETITEMDATA, index, 0);
|
||||
return value == CB_ERR ? fallback : static_cast<int>(value);
|
||||
}
|
||||
|
||||
void layout(State& state, int width, int height) {
|
||||
const int margin = px(state.dpi, 16);
|
||||
const int labelWidth = px(state.dpi, 158);
|
||||
const int fieldWidth = px(state.dpi, 130);
|
||||
const int rowHeight = px(state.dpi, 27);
|
||||
const int lineGap = px(state.dpi, 11);
|
||||
const int groupHeight = height - margin * 3 - px(state.dpi, 34);
|
||||
MoveWindow(state.group, margin, margin, width - margin * 2, groupHeight, TRUE);
|
||||
|
||||
const int left = margin + px(state.dpi, 14);
|
||||
const int fieldLeft = left + labelWidth;
|
||||
int y = margin + px(state.dpi, 28);
|
||||
MoveWindow(state.fpsLabel, left, y + px(state.dpi, 4), labelWidth, rowHeight, TRUE);
|
||||
MoveWindow(state.fps, fieldLeft, y, fieldWidth, px(state.dpi, 180), TRUE);
|
||||
y += rowHeight + lineGap;
|
||||
|
||||
MoveWindow(state.qualityLabel, left, y + px(state.dpi, 4), labelWidth, rowHeight, TRUE);
|
||||
MoveWindow(state.quality, fieldLeft, y, fieldWidth, px(state.dpi, 220), TRUE);
|
||||
y += rowHeight + lineGap;
|
||||
|
||||
MoveWindow(state.durationLabel, left, y + px(state.dpi, 4), labelWidth, rowHeight, TRUE);
|
||||
MoveWindow(state.duration, fieldLeft, y, fieldWidth, rowHeight, TRUE);
|
||||
y += rowHeight + px(state.dpi, 4);
|
||||
MoveWindow(state.durationHint, fieldLeft, y, width - fieldLeft - margin * 2,
|
||||
px(state.dpi, 34), TRUE);
|
||||
y += px(state.dpi, 42);
|
||||
MoveWindow(state.cursor, left, y, width - left - margin * 2, rowHeight, TRUE);
|
||||
|
||||
const int buttonWidth = px(state.dpi, 104);
|
||||
const int buttonHeight = px(state.dpi, 31);
|
||||
const int buttonsY = height - margin - buttonHeight;
|
||||
MoveWindow(state.cancel, width - margin - buttonWidth, buttonsY,
|
||||
buttonWidth, buttonHeight, TRUE);
|
||||
MoveWindow(state.start, width - margin * 2 - buttonWidth * 2, buttonsY,
|
||||
buttonWidth, buttonHeight, TRUE);
|
||||
}
|
||||
|
||||
bool readAndValidate(State& state) {
|
||||
wchar_t durationText[32]{};
|
||||
GetWindowTextW(state.duration, durationText, static_cast<int>(std::size(durationText)));
|
||||
wchar_t* end = nullptr;
|
||||
const long duration = std::wcstol(durationText, &end, 10);
|
||||
if (!end || *end != L'\0' || duration < 0 ||
|
||||
duration > state.options.hardLimitSeconds) {
|
||||
const std::wstring message =
|
||||
L"Длительность должна быть от 0 до " +
|
||||
std::to_wstring(state.options.hardLimitSeconds) +
|
||||
L" секунд. 0 — остановка вручную.";
|
||||
MessageBoxW(state.window, message.c_str(), L"NightShot — GIF",
|
||||
MB_OK | MB_ICONWARNING);
|
||||
SetFocus(state.duration);
|
||||
return false;
|
||||
}
|
||||
|
||||
state.options.fps = comboValue(state.fps, 30);
|
||||
state.options.quality = comboValue(state.quality, 8);
|
||||
state.options.durationSeconds = static_cast<int>(duration);
|
||||
state.options.captureCursor =
|
||||
SendMessageW(state.cursor, BM_GETCHECK, 0, 0) == BST_CHECKED;
|
||||
return true;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
||||
auto* state = reinterpret_cast<State*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||
if (message == WM_NCCREATE) {
|
||||
const auto* create = reinterpret_cast<const CREATESTRUCTW*>(lParam);
|
||||
state = reinterpret_cast<State*>(create->lpCreateParams);
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(state));
|
||||
state->window = hwnd;
|
||||
}
|
||||
if (!state) return DefWindowProcW(hwnd, message, wParam, lParam);
|
||||
|
||||
switch (message) {
|
||||
case WM_CREATE: {
|
||||
replaceFont(*state, GetDpiForWindow(hwnd));
|
||||
state->group = createControl(*state, L"BUTTON", L"Параметры записи",
|
||||
BS_GROUPBOX, 0);
|
||||
state->fpsLabel = createControl(*state, L"STATIC", L"Кадров в секунду:", SS_LEFT, 0);
|
||||
state->fps = createControl(*state, L"COMBOBOX", L"",
|
||||
CBS_DROPDOWNLIST | WS_TABSTOP | WS_VSCROLL,
|
||||
kIdFps);
|
||||
for (const int fps : {15, 30, 45, 60}) {
|
||||
addComboValue(state->fps, std::to_wstring(fps) + L" FPS", fps);
|
||||
}
|
||||
selectComboValue(state->fps, state->options.fps);
|
||||
|
||||
state->qualityLabel = createControl(*state, L"STATIC", L"Качество GIF:", SS_LEFT, 0);
|
||||
state->quality = createControl(*state, L"COMBOBOX", L"",
|
||||
CBS_DROPDOWNLIST | WS_TABSTOP | WS_VSCROLL,
|
||||
kIdQuality);
|
||||
for (int quality = 1; quality <= 10; ++quality) {
|
||||
std::wstring label = std::to_wstring(quality) + L" / 10";
|
||||
if (quality == 8) label += L" — рекомендуется";
|
||||
addComboValue(state->quality, label, quality);
|
||||
}
|
||||
selectComboValue(state->quality, state->options.quality);
|
||||
|
||||
state->durationLabel = createControl(*state, L"STATIC", L"Длительность, сек:", SS_LEFT, 0);
|
||||
state->duration = createControl(*state, L"EDIT",
|
||||
std::to_wstring(state->options.durationSeconds).c_str(),
|
||||
ES_NUMBER | ES_AUTOHSCROLL | WS_TABSTOP,
|
||||
kIdDuration, WS_EX_CLIENTEDGE);
|
||||
const std::wstring hint = L"0 — до повторного Ctrl+PrintScreen. Максимум: " +
|
||||
std::to_wstring(state->options.hardLimitSeconds) + L" сек.";
|
||||
state->durationHint = createControl(*state, L"STATIC", hint.c_str(), SS_LEFT, 0);
|
||||
state->cursor = createControl(*state, L"BUTTON", L"Записывать курсор мыши",
|
||||
BS_AUTOCHECKBOX | WS_TABSTOP, kIdCursor);
|
||||
SendMessageW(state->cursor, BM_SETCHECK,
|
||||
state->options.captureCursor ? BST_CHECKED : BST_UNCHECKED, 0);
|
||||
state->start = createControl(*state, L"BUTTON", L"Начать запись",
|
||||
BS_DEFPUSHBUTTON | WS_TABSTOP, kIdStart);
|
||||
state->cancel = createControl(*state, L"BUTTON", L"Отмена",
|
||||
BS_PUSHBUTTON | WS_TABSTOP, kIdCancel);
|
||||
RECT client{};
|
||||
GetClientRect(hwnd, &client);
|
||||
layout(*state, client.right, client.bottom);
|
||||
return 0;
|
||||
}
|
||||
case WM_SIZE:
|
||||
layout(*state, LOWORD(lParam), HIWORD(lParam));
|
||||
return 0;
|
||||
case WM_DPICHANGED: {
|
||||
replaceFont(*state, HIWORD(wParam));
|
||||
const RECT* suggested = reinterpret_cast<const RECT*>(lParam);
|
||||
SetWindowPos(hwnd, nullptr, suggested->left, suggested->top,
|
||||
suggested->right - suggested->left,
|
||||
suggested->bottom - suggested->top,
|
||||
SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
return 0;
|
||||
}
|
||||
case WM_COMMAND:
|
||||
switch (LOWORD(wParam)) {
|
||||
case kIdStart:
|
||||
if (readAndValidate(*state)) {
|
||||
state->accepted = true;
|
||||
DestroyWindow(hwnd);
|
||||
}
|
||||
return 0;
|
||||
case kIdCancel:
|
||||
DestroyWindow(hwnd);
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_CLOSE:
|
||||
DestroyWindow(hwnd);
|
||||
return 0;
|
||||
case WM_DESTROY:
|
||||
if (state->font) DeleteObject(state->font);
|
||||
state->font = nullptr;
|
||||
state->finished = true;
|
||||
if (state->owner && IsWindow(state->owner)) {
|
||||
EnableWindow(state->owner, TRUE);
|
||||
SetForegroundWindow(state->owner);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProcW(hwnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
void ensureClass() {
|
||||
if (g_class) return;
|
||||
WNDCLASSEXW wc{};
|
||||
wc.cbSize = sizeof(wc);
|
||||
wc.lpfnWndProc = windowProc;
|
||||
wc.hInstance = GetModuleHandleW(nullptr);
|
||||
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||
wc.hIcon = static_cast<HICON>(LoadImageW(
|
||||
wc.hInstance, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON,
|
||||
0, 0, LR_DEFAULTSIZE));
|
||||
if (!wc.hIcon) wc.hIcon = LoadIconW(nullptr, IDI_APPLICATION);
|
||||
wc.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_BTNFACE + 1);
|
||||
wc.lpszClassName = kClassName;
|
||||
g_class = RegisterClassExW(&wc);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool show(HWND owner, const settings::AppSettings& defaults,
|
||||
gif_recorder::Options& options) {
|
||||
ensureClass();
|
||||
if (!g_class) return false;
|
||||
|
||||
State state;
|
||||
state.owner = owner;
|
||||
state.options.fps = defaults.gifFps;
|
||||
state.options.quality = defaults.gifQuality;
|
||||
state.options.durationSeconds = 0;
|
||||
state.options.hardLimitSeconds = defaults.gifMaxDurationSeconds;
|
||||
state.options.captureCursor = defaults.gifCaptureCursor;
|
||||
|
||||
const UINT dpi = owner && IsWindow(owner) ? GetDpiForWindow(owner) : 96;
|
||||
const int width = px(dpi, 440);
|
||||
const int height = px(dpi, 300);
|
||||
|
||||
RECT work{};
|
||||
MONITORINFO monitorInfo{};
|
||||
monitorInfo.cbSize = sizeof(monitorInfo);
|
||||
const HMONITOR monitor = MonitorFromWindow(owner, MONITOR_DEFAULTTONEAREST);
|
||||
if (GetMonitorInfoW(monitor, &monitorInfo)) work = monitorInfo.rcWork;
|
||||
else SystemParametersInfoW(SPI_GETWORKAREA, 0, &work, 0);
|
||||
const int x = work.left + std::max<LONG>(0, ((work.right - work.left) - width) / 2);
|
||||
const int y = work.top + std::max<LONG>(0, ((work.bottom - work.top) - height) / 2);
|
||||
|
||||
HWND window = CreateWindowExW(
|
||||
WS_EX_DLGMODALFRAME | WS_EX_CONTROLPARENT | WS_EX_TOPMOST,
|
||||
kClassName, L"NightShot — запись GIF",
|
||||
WS_CAPTION | WS_SYSMENU | WS_POPUP,
|
||||
x, y, width, height, owner, nullptr,
|
||||
GetModuleHandleW(nullptr), &state);
|
||||
if (!window) return false;
|
||||
|
||||
if (owner && IsWindow(owner)) EnableWindow(owner, FALSE);
|
||||
ShowWindow(window, SW_SHOWNORMAL);
|
||||
UpdateWindow(window);
|
||||
SetForegroundWindow(window);
|
||||
|
||||
MSG message{};
|
||||
while (!state.finished) {
|
||||
const BOOL result = GetMessageW(&message, nullptr, 0, 0);
|
||||
if (result <= 0) {
|
||||
if (result == 0) PostQuitMessage(static_cast<int>(message.wParam));
|
||||
break;
|
||||
}
|
||||
if (!IsDialogMessageW(window, &message)) {
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
}
|
||||
}
|
||||
|
||||
if (state.accepted) options = state.options;
|
||||
return state.accepted;
|
||||
}
|
||||
|
||||
} // namespace gif_options_window
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,450 @@
|
||||
#include "image_io.hpp"
|
||||
#include <commdlg.h>
|
||||
#include <shlobj.h>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
int findEncoderClsid(const WCHAR* mimeType, CLSID* clsid) {
|
||||
UINT count = 0;
|
||||
UINT bytes = 0;
|
||||
if (Gdiplus::GetImageEncodersSize(&count, &bytes) != Gdiplus::Ok || bytes == 0) return -1;
|
||||
std::vector<BYTE> storage(bytes);
|
||||
auto* codecs = reinterpret_cast<Gdiplus::ImageCodecInfo*>(storage.data());
|
||||
if (Gdiplus::GetImageEncoders(count, bytes, codecs) != Gdiplus::Ok) return -1;
|
||||
for (UINT i = 0; i < count; ++i) {
|
||||
if (wcscmp(codecs[i].MimeType, mimeType) == 0) {
|
||||
*clsid = codecs[i].Clsid;
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool saveBitmapAsPng(Gdiplus::Bitmap& bitmap, const std::wstring& path) {
|
||||
CLSID png{};
|
||||
if (findEncoderClsid(L"image/png", &png) < 0) return false;
|
||||
return bitmap.Save(path.c_str(), &png, nullptr) == Gdiplus::Ok;
|
||||
}
|
||||
|
||||
bool saveBitmapAsJpeg(Gdiplus::Bitmap& bitmap, const std::wstring& path, int quality) {
|
||||
CLSID jpeg{};
|
||||
if (findEncoderClsid(L"image/jpeg", &jpeg) < 0) return false;
|
||||
|
||||
ULONG value = static_cast<ULONG>(std::clamp(quality, 1, 100));
|
||||
Gdiplus::EncoderParameters parameters{};
|
||||
parameters.Count = 1;
|
||||
parameters.Parameter[0].Guid = Gdiplus::EncoderQuality;
|
||||
parameters.Parameter[0].Type = Gdiplus::EncoderParameterValueTypeLong;
|
||||
parameters.Parameter[0].NumberOfValues = 1;
|
||||
parameters.Parameter[0].Value = &value;
|
||||
return bitmap.Save(path.c_str(), &jpeg, ¶meters) == Gdiplus::Ok;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// MinGW defines LCS_sRGB using a multi-character literal ('sRGB'), which
|
||||
// is implementation-defined and trips -Wmultichar. This is the exact DWORD
|
||||
// value from wingdi.h, written explicitly so the build stays warning-free.
|
||||
constexpr DWORD kLcsSrgb = 0x73524742UL;
|
||||
|
||||
struct OpaqueBgraImage {
|
||||
UINT width = 0;
|
||||
UINT height = 0;
|
||||
INT stride = 0;
|
||||
std::vector<BYTE> pixels;
|
||||
|
||||
[[nodiscard]] bool valid() const {
|
||||
return width > 0 && height > 0 && stride > 0 && !pixels.empty();
|
||||
}
|
||||
};
|
||||
|
||||
OpaqueBgraImage renderOpaqueBgra(Gdiplus::Bitmap& source) {
|
||||
OpaqueBgraImage result;
|
||||
result.width = source.GetWidth();
|
||||
result.height = source.GetHeight();
|
||||
if (result.width == 0 || result.height == 0 ||
|
||||
result.width > static_cast<UINT>(std::numeric_limits<INT>::max() / 4)) return result;
|
||||
|
||||
result.stride = static_cast<INT>(result.width * 4u);
|
||||
const std::size_t byteCount = static_cast<std::size_t>(result.stride) * result.height;
|
||||
if (byteCount / static_cast<std::size_t>(result.stride) != result.height) return {};
|
||||
|
||||
result.pixels.assign(byteCount, 0);
|
||||
Gdiplus::Bitmap target(static_cast<INT>(result.width), static_cast<INT>(result.height), result.stride,
|
||||
PixelFormat32bppARGB, result.pixels.data());
|
||||
if (target.GetLastStatus() != Gdiplus::Ok) return {};
|
||||
|
||||
Gdiplus::Graphics graphics(&target);
|
||||
graphics.SetCompositingMode(Gdiplus::CompositingModeSourceCopy);
|
||||
graphics.Clear(Gdiplus::Color(255, 255, 255, 255));
|
||||
graphics.SetCompositingMode(Gdiplus::CompositingModeSourceOver);
|
||||
graphics.SetInterpolationMode(Gdiplus::InterpolationModeNearestNeighbor);
|
||||
if (graphics.DrawImage(&source, 0, 0, static_cast<INT>(result.width),
|
||||
static_cast<INT>(result.height)) != Gdiplus::Ok) return {};
|
||||
graphics.Flush(Gdiplus::FlushIntentionSync);
|
||||
|
||||
// The screenshot is composited onto opaque white. Force alpha to 255 so
|
||||
// applications that interpret CF_DIBV5 alpha do not paste a translucent
|
||||
// or completely black image.
|
||||
for (std::size_t offset = 3; offset < result.pixels.size(); offset += 4) {
|
||||
result.pixels[offset] = 255;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
HGLOBAL makeDibV5(const OpaqueBgraImage& image) {
|
||||
if (!image.valid()) return nullptr;
|
||||
const std::size_t pixelBytes = static_cast<std::size_t>(image.stride) * image.height;
|
||||
const std::size_t totalBytes = sizeof(BITMAPV5HEADER) + pixelBytes;
|
||||
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, totalBytes);
|
||||
if (!memory) return nullptr;
|
||||
|
||||
auto* bytes = static_cast<BYTE*>(GlobalLock(memory));
|
||||
if (!bytes) {
|
||||
GlobalFree(memory);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* header = reinterpret_cast<BITMAPV5HEADER*>(bytes);
|
||||
header->bV5Size = sizeof(BITMAPV5HEADER);
|
||||
header->bV5Width = static_cast<LONG>(image.width);
|
||||
header->bV5Height = -static_cast<LONG>(image.height); // top-down
|
||||
header->bV5Planes = 1;
|
||||
header->bV5BitCount = 32;
|
||||
header->bV5Compression = BI_BITFIELDS;
|
||||
header->bV5SizeImage = static_cast<DWORD>(pixelBytes);
|
||||
header->bV5RedMask = 0x00FF0000;
|
||||
header->bV5GreenMask = 0x0000FF00;
|
||||
header->bV5BlueMask = 0x000000FF;
|
||||
header->bV5AlphaMask = 0xFF000000;
|
||||
header->bV5CSType = kLcsSrgb;
|
||||
header->bV5Intent = LCS_GM_IMAGES;
|
||||
std::memcpy(bytes + sizeof(BITMAPV5HEADER), image.pixels.data(), pixelBytes);
|
||||
GlobalUnlock(memory);
|
||||
return memory;
|
||||
}
|
||||
|
||||
HGLOBAL makeDib24(const OpaqueBgraImage& image) {
|
||||
if (!image.valid()) return nullptr;
|
||||
const std::size_t rowBytes = (static_cast<std::size_t>(image.width) * 3u + 3u) & ~std::size_t(3u);
|
||||
const std::size_t pixelBytes = rowBytes * image.height;
|
||||
const std::size_t totalBytes = sizeof(BITMAPINFOHEADER) + pixelBytes;
|
||||
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, totalBytes);
|
||||
if (!memory) return nullptr;
|
||||
|
||||
auto* bytes = static_cast<BYTE*>(GlobalLock(memory));
|
||||
if (!bytes) {
|
||||
GlobalFree(memory);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* header = reinterpret_cast<BITMAPINFOHEADER*>(bytes);
|
||||
header->biSize = sizeof(BITMAPINFOHEADER);
|
||||
header->biWidth = static_cast<LONG>(image.width);
|
||||
header->biHeight = static_cast<LONG>(image.height); // bottom-up for broad compatibility
|
||||
header->biPlanes = 1;
|
||||
header->biBitCount = 24;
|
||||
header->biCompression = BI_RGB;
|
||||
header->biSizeImage = static_cast<DWORD>(pixelBytes);
|
||||
|
||||
BYTE* destination = bytes + sizeof(BITMAPINFOHEADER);
|
||||
for (UINT y = 0; y < image.height; ++y) {
|
||||
const BYTE* sourceRow = image.pixels.data() + static_cast<std::size_t>(y) * image.stride;
|
||||
BYTE* destinationRow = destination + static_cast<std::size_t>(image.height - 1u - y) * rowBytes;
|
||||
for (UINT x = 0; x < image.width; ++x) {
|
||||
destinationRow[x * 3u + 0u] = sourceRow[x * 4u + 0u];
|
||||
destinationRow[x * 3u + 1u] = sourceRow[x * 4u + 1u];
|
||||
destinationRow[x * 3u + 2u] = sourceRow[x * 4u + 2u];
|
||||
}
|
||||
}
|
||||
|
||||
GlobalUnlock(memory);
|
||||
return memory;
|
||||
}
|
||||
|
||||
HBITMAP makeClipboardBitmap(const OpaqueBgraImage& image) {
|
||||
if (!image.valid()) return nullptr;
|
||||
BITMAPINFO info{};
|
||||
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||
info.bmiHeader.biWidth = static_cast<LONG>(image.width);
|
||||
info.bmiHeader.biHeight = -static_cast<LONG>(image.height);
|
||||
info.bmiHeader.biPlanes = 1;
|
||||
info.bmiHeader.biBitCount = 32;
|
||||
info.bmiHeader.biCompression = BI_RGB;
|
||||
|
||||
void* pixels = nullptr;
|
||||
HDC screen = GetDC(nullptr);
|
||||
HBITMAP bitmap = CreateDIBSection(screen, &info, DIB_RGB_COLORS, &pixels, nullptr, 0);
|
||||
if (screen) ReleaseDC(nullptr, screen);
|
||||
if (!bitmap || !pixels) {
|
||||
if (bitmap) DeleteObject(bitmap);
|
||||
return nullptr;
|
||||
}
|
||||
std::memcpy(pixels, image.pixels.data(), static_cast<std::size_t>(image.stride) * image.height);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
HGLOBAL makePng(OpaqueBgraImage& image) {
|
||||
if (!image.valid()) return nullptr;
|
||||
CLSID encoder{};
|
||||
if (findEncoderClsid(L"image/png", &encoder) < 0) return nullptr;
|
||||
|
||||
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, 0);
|
||||
if (!memory) return nullptr;
|
||||
|
||||
IStream* stream = nullptr;
|
||||
if (FAILED(CreateStreamOnHGlobal(memory, FALSE, &stream)) || !stream) {
|
||||
GlobalFree(memory);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Gdiplus::Bitmap bitmap(static_cast<INT>(image.width), static_cast<INT>(image.height), image.stride,
|
||||
PixelFormat32bppARGB, image.pixels.data());
|
||||
const bool saved = bitmap.GetLastStatus() == Gdiplus::Ok &&
|
||||
bitmap.Save(stream, &encoder, nullptr) == Gdiplus::Ok;
|
||||
stream->Release();
|
||||
if (!saved) {
|
||||
GlobalFree(memory);
|
||||
return nullptr;
|
||||
}
|
||||
return memory;
|
||||
}
|
||||
|
||||
bool openClipboardWithRetry(HWND owner) {
|
||||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||
if (OpenClipboard(owner)) return true;
|
||||
Sleep(12 + attempt * 4);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap) {
|
||||
OpaqueBgraImage image = renderOpaqueBgra(bitmap);
|
||||
if (!image.valid()) return false;
|
||||
|
||||
HGLOBAL png = makePng(image);
|
||||
HGLOBAL dibV5 = makeDibV5(image);
|
||||
HGLOBAL dib = makeDib24(image);
|
||||
HBITMAP nativeBitmap = makeClipboardBitmap(image);
|
||||
if (!png && !dibV5 && !dib && !nativeBitmap) return false;
|
||||
|
||||
if (!openClipboardWithRetry(owner)) {
|
||||
if (png) GlobalFree(png);
|
||||
if (dibV5) GlobalFree(dibV5);
|
||||
if (dib) GlobalFree(dib);
|
||||
if (nativeBitmap) DeleteObject(nativeBitmap);
|
||||
return false;
|
||||
}
|
||||
if (!EmptyClipboard()) {
|
||||
CloseClipboard();
|
||||
if (png) GlobalFree(png);
|
||||
if (dibV5) GlobalFree(dibV5);
|
||||
if (dib) GlobalFree(dib);
|
||||
if (nativeBitmap) DeleteObject(nativeBitmap);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool transferred = false;
|
||||
const UINT pngFormat = RegisterClipboardFormatW(L"PNG");
|
||||
if (png && pngFormat && SetClipboardData(pngFormat, png)) {
|
||||
png = nullptr;
|
||||
transferred = true;
|
||||
}
|
||||
if (dibV5 && SetClipboardData(CF_DIBV5, dibV5)) {
|
||||
dibV5 = nullptr;
|
||||
transferred = true;
|
||||
}
|
||||
if (dib && SetClipboardData(CF_DIB, dib)) {
|
||||
dib = nullptr;
|
||||
transferred = true;
|
||||
}
|
||||
if (nativeBitmap && SetClipboardData(CF_BITMAP, nativeBitmap)) {
|
||||
nativeBitmap = nullptr;
|
||||
transferred = true;
|
||||
}
|
||||
CloseClipboard();
|
||||
|
||||
if (png) GlobalFree(png);
|
||||
if (dibV5) GlobalFree(dibV5);
|
||||
if (dib) GlobalFree(dib);
|
||||
if (nativeBitmap) DeleteObject(nativeBitmap);
|
||||
return transferred;
|
||||
}
|
||||
|
||||
bool encodeBitmapAsPng(Gdiplus::Bitmap& bitmap, std::vector<BYTE>& output) {
|
||||
output.clear();
|
||||
OpaqueBgraImage image = renderOpaqueBgra(bitmap);
|
||||
if (!image.valid()) return false;
|
||||
|
||||
CLSID encoder{};
|
||||
if (findEncoderClsid(L"image/png", &encoder) < 0) return false;
|
||||
|
||||
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, 0);
|
||||
if (!memory) return false;
|
||||
|
||||
IStream* stream = nullptr;
|
||||
if (FAILED(CreateStreamOnHGlobal(memory, FALSE, &stream)) || !stream) {
|
||||
GlobalFree(memory);
|
||||
return false;
|
||||
}
|
||||
|
||||
Gdiplus::Bitmap encoded(static_cast<INT>(image.width),
|
||||
static_cast<INT>(image.height), image.stride,
|
||||
PixelFormat32bppARGB, image.pixels.data());
|
||||
bool ok = encoded.GetLastStatus() == Gdiplus::Ok &&
|
||||
encoded.Save(stream, &encoder, nullptr) == Gdiplus::Ok;
|
||||
|
||||
STATSTG statistics{};
|
||||
if (ok) ok = SUCCEEDED(stream->Stat(&statistics, STATFLAG_NONAME));
|
||||
const ULONGLONG logicalBytes = statistics.cbSize.QuadPart;
|
||||
if (ok) {
|
||||
ok = logicalBytes > 0 &&
|
||||
logicalBytes <= static_cast<ULONGLONG>(
|
||||
std::numeric_limits<std::size_t>::max());
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
const void* data = GlobalLock(memory);
|
||||
if (!data) {
|
||||
ok = false;
|
||||
} else {
|
||||
const auto* begin = static_cast<const BYTE*>(data);
|
||||
output.assign(begin, begin + static_cast<std::size_t>(logicalBytes));
|
||||
GlobalUnlock(memory);
|
||||
}
|
||||
}
|
||||
|
||||
stream->Release();
|
||||
GlobalFree(memory);
|
||||
if (!ok) output.clear();
|
||||
return ok && !output.empty();
|
||||
}
|
||||
|
||||
|
||||
std::wstring reserveIncrementedPath(
|
||||
const std::wstring& directory, const wchar_t* prefix,
|
||||
const wchar_t* extension) {
|
||||
if (directory.empty() || !prefix || !*prefix || !extension || !*extension) return {};
|
||||
|
||||
std::error_code error;
|
||||
std::filesystem::create_directories(directory, error);
|
||||
if (error) return {};
|
||||
|
||||
for (unsigned int number = 1; number < 1000000000u; ++number) {
|
||||
std::filesystem::path candidate(directory);
|
||||
candidate /= std::wstring(prefix) + std::to_wstring(number) + extension;
|
||||
HANDLE file = CreateFileW(
|
||||
candidate.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW,
|
||||
FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (file != INVALID_HANDLE_VALUE) {
|
||||
CloseHandle(file);
|
||||
return candidate.wstring();
|
||||
}
|
||||
const DWORD lastError = GetLastError();
|
||||
if (lastError != ERROR_FILE_EXISTS && lastError != ERROR_ALREADY_EXISTS) return {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool saveScreenshotIncremented(
|
||||
Gdiplus::Bitmap& bitmap, const settings::AppSettings& configuration,
|
||||
std::wstring& savedPath) {
|
||||
savedPath.clear();
|
||||
const std::wstring directory = configuration.saveDirectory.empty()
|
||||
? settings::defaultSaveDirectory() : configuration.saveDirectory;
|
||||
const bool jpeg = configuration.screenshotFormat == settings::ScreenshotFormat::Jpeg;
|
||||
const std::wstring path = reserveIncrementedPath(
|
||||
directory, L"Screenshot_", jpeg ? L".jpg" : L".png");
|
||||
if (path.empty()) return false;
|
||||
|
||||
const bool saved = jpeg
|
||||
? saveBitmapAsJpeg(bitmap, path, configuration.jpegQuality)
|
||||
: saveBitmapAsPng(bitmap, path);
|
||||
if (!saved) {
|
||||
DeleteFileW(path.c_str());
|
||||
return false;
|
||||
}
|
||||
savedPath = path;
|
||||
return true;
|
||||
}
|
||||
|
||||
PrintBitmapResult printBitmap(HWND owner, Gdiplus::Bitmap& bitmap) {
|
||||
PRINTDLGW dialog{};
|
||||
dialog.lStructSize = sizeof(dialog);
|
||||
dialog.hwndOwner = owner;
|
||||
dialog.Flags = PD_RETURNDC | PD_NOPAGENUMS | PD_NOSELECTION | PD_USEDEVMODECOPIESANDCOLLATE;
|
||||
if (!PrintDlgW(&dialog)) return CommDlgExtendedError() == 0 ? PrintBitmapResult::Cancelled : PrintBitmapResult::Failed;
|
||||
if (!dialog.hDC) return PrintBitmapResult::Failed;
|
||||
|
||||
DOCINFOW document{};
|
||||
document.cbSize = sizeof(document);
|
||||
document.lpszDocName = L"NightShot screenshot";
|
||||
bool ok = StartDocW(dialog.hDC, &document) > 0;
|
||||
if (ok) ok = StartPage(dialog.hDC) > 0;
|
||||
if (ok) {
|
||||
const int pageWidth = GetDeviceCaps(dialog.hDC, HORZRES);
|
||||
const int pageHeight = GetDeviceCaps(dialog.hDC, VERTRES);
|
||||
const int marginX = std::max(1, pageWidth / 20);
|
||||
const int marginY = std::max(1, pageHeight / 20);
|
||||
const int availableWidth = std::max(1, pageWidth - marginX * 2);
|
||||
const int availableHeight = std::max(1, pageHeight - marginY * 2);
|
||||
const double scale = std::min(static_cast<double>(availableWidth) / std::max<UINT>(1, bitmap.GetWidth()),
|
||||
static_cast<double>(availableHeight) / std::max<UINT>(1, bitmap.GetHeight()));
|
||||
const int drawWidth = std::max(1, static_cast<int>(bitmap.GetWidth() * scale));
|
||||
const int drawHeight = std::max(1, static_cast<int>(bitmap.GetHeight() * scale));
|
||||
const int x = (pageWidth - drawWidth) / 2;
|
||||
const int y = (pageHeight - drawHeight) / 2;
|
||||
Gdiplus::Graphics graphics(dialog.hDC);
|
||||
graphics.SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic);
|
||||
ok = graphics.DrawImage(&bitmap, x, y, drawWidth, drawHeight) == Gdiplus::Ok;
|
||||
}
|
||||
if (ok) ok = EndPage(dialog.hDC) > 0;
|
||||
if (ok) ok = EndDoc(dialog.hDC) > 0;
|
||||
else AbortDoc(dialog.hDC);
|
||||
|
||||
DeleteDC(dialog.hDC);
|
||||
if (dialog.hDevMode) GlobalFree(dialog.hDevMode);
|
||||
if (dialog.hDevNames) GlobalFree(dialog.hDevNames);
|
||||
return ok ? PrintBitmapResult::Printed : PrintBitmapResult::Failed;
|
||||
}
|
||||
|
||||
static std::wstring defaultFileName() {
|
||||
SYSTEMTIME st{};
|
||||
GetLocalTime(&st);
|
||||
std::wostringstream out;
|
||||
out << L"Screenshot_" << std::setfill(L'0')
|
||||
<< std::setw(4) << st.wYear << L'-' << std::setw(2) << st.wMonth << L'-' << std::setw(2) << st.wDay
|
||||
<< L'_' << std::setw(2) << st.wHour << L'-' << std::setw(2) << st.wMinute << L'-' << std::setw(2) << st.wSecond
|
||||
<< L".png";
|
||||
return out.str();
|
||||
}
|
||||
|
||||
std::wstring choosePngPath(HWND owner) {
|
||||
std::wstring initialDirectory;
|
||||
PWSTR pictures = nullptr;
|
||||
if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_Pictures, KF_FLAG_DEFAULT, nullptr, &pictures))) {
|
||||
initialDirectory = pictures;
|
||||
CoTaskMemFree(pictures);
|
||||
}
|
||||
|
||||
std::wstring file = defaultFileName();
|
||||
std::vector<wchar_t> buffer(32768, L'\0');
|
||||
wcsncpy_s(buffer.data(), buffer.size(), file.c_str(), _TRUNCATE);
|
||||
|
||||
OPENFILENAMEW ofn{};
|
||||
ofn.lStructSize = sizeof(ofn);
|
||||
ofn.hwndOwner = owner;
|
||||
ofn.lpstrFilter = L"PNG image (*.png)\0*.png\0All files (*.*)\0*.*\0\0";
|
||||
ofn.lpstrFile = buffer.data();
|
||||
ofn.nMaxFile = static_cast<DWORD>(buffer.size());
|
||||
ofn.lpstrInitialDir = initialDirectory.empty() ? nullptr : initialDirectory.c_str();
|
||||
ofn.lpstrDefExt = L"png";
|
||||
ofn.Flags = OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
|
||||
return GetSaveFileNameW(&ofn) ? std::wstring(buffer.data()) : std::wstring();
|
||||
}
|
||||
+499
@@ -0,0 +1,499 @@
|
||||
#include "config.hpp"
|
||||
#include "app_messages.hpp"
|
||||
#include "capture.hpp"
|
||||
#include "gif_recorder.hpp"
|
||||
#include "notifications.hpp"
|
||||
#include "overlay.hpp"
|
||||
#include "resource.h"
|
||||
#include "settings.hpp"
|
||||
#include "settings_window.hpp"
|
||||
#include "startup.hpp"
|
||||
#include "uploader.hpp"
|
||||
|
||||
#include <objidl.h>
|
||||
#include <gdiplus.h>
|
||||
#include <shellapi.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <cwchar>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
constexpr wchar_t kMainClass[] = L"NightShotMainWindow";
|
||||
constexpr wchar_t kMutexName[] = L"Local\\NightShot.Win64.Singleton";
|
||||
constexpr UINT kTrayMessage = WM_APP + 10;
|
||||
constexpr UINT kStartCapture = WM_APP + 11;
|
||||
constexpr UINT kToggleGifCapture = WM_APP + 19;
|
||||
constexpr UINT kTrayId = 1;
|
||||
constexpr int kHotkeyScreenshotId = 1;
|
||||
constexpr int kHotkeyGifId = 2;
|
||||
constexpr UINT kMenuCapture = 1001;
|
||||
constexpr UINT kMenuGif = 1002;
|
||||
constexpr UINT kMenuSettings = 1003;
|
||||
constexpr UINT kMenuExit = 1004;
|
||||
|
||||
HWND g_mainWindow = nullptr;
|
||||
HHOOK g_keyboardHook = nullptr;
|
||||
UINT g_taskbarCreated = 0;
|
||||
HWND g_overlay = nullptr;
|
||||
bool g_hotkeysRegistered = false;
|
||||
bool g_printScreenDown = false;
|
||||
|
||||
template <std::size_t N>
|
||||
void copyTruncated(wchar_t (&destination)[N], const wchar_t* source) {
|
||||
if constexpr (N == 0) return;
|
||||
const wchar_t* text = source ? source : L"";
|
||||
const std::size_t length =
|
||||
std::min<std::size_t>(std::wcslen(text), N - 1);
|
||||
std::wmemcpy(destination, text, length);
|
||||
destination[length] = L'\0';
|
||||
}
|
||||
|
||||
void addTrayIcon(HWND hwnd) {
|
||||
NOTIFYICONDATAW data{};
|
||||
data.cbSize = sizeof(data);
|
||||
data.hWnd = hwnd;
|
||||
data.uID = kTrayId;
|
||||
data.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
|
||||
data.uCallbackMessage = kTrayMessage;
|
||||
data.hIcon = static_cast<HICON>(LoadImageW(
|
||||
GetModuleHandleW(nullptr), MAKEINTRESOURCEW(IDI_APP_ICON),
|
||||
IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
|
||||
if (!data.hIcon) data.hIcon = LoadIconW(nullptr, IDI_APPLICATION);
|
||||
copyTruncated(data.szTip, L"NightShot — PrintScreen");
|
||||
Shell_NotifyIconW(NIM_ADD, &data);
|
||||
data.uVersion = NOTIFYICON_VERSION_4;
|
||||
Shell_NotifyIconW(NIM_SETVERSION, &data);
|
||||
}
|
||||
|
||||
void removeTrayIcon(HWND hwnd) {
|
||||
NOTIFYICONDATAW data{};
|
||||
data.cbSize = sizeof(data);
|
||||
data.hWnd = hwnd;
|
||||
data.uID = kTrayId;
|
||||
Shell_NotifyIconW(NIM_DELETE, &data);
|
||||
}
|
||||
|
||||
void showTrayNotification(HWND hwnd, const wchar_t* title, const wchar_t* body,
|
||||
DWORD flags = NIIF_INFO) {
|
||||
NOTIFYICONDATAW data{};
|
||||
data.cbSize = sizeof(data);
|
||||
data.hWnd = hwnd;
|
||||
data.uID = kTrayId;
|
||||
data.uFlags = NIF_INFO;
|
||||
data.dwInfoFlags = flags | NIIF_NOSOUND | NIIF_RESPECT_QUIET_TIME;
|
||||
copyTruncated(data.szInfoTitle, title ? title : L"NightShot");
|
||||
copyTruncated(data.szInfo, body ? body : L"");
|
||||
Shell_NotifyIconW(NIM_MODIFY, &data);
|
||||
}
|
||||
|
||||
void notifyIfEnabled(HWND hwnd, const wchar_t* title, const wchar_t* body,
|
||||
DWORD fallbackFlags = NIIF_INFO) {
|
||||
if (!settings::load().notificationsEnabled) return;
|
||||
if (!notifications::show(title, body)) {
|
||||
showTrayNotification(hwnd, title, body, fallbackFlags);
|
||||
}
|
||||
}
|
||||
|
||||
bool copyTextToClipboard(HWND owner, const std::wstring& text) {
|
||||
if (text.empty()) return false;
|
||||
const SIZE_T bytes = (text.size() + 1) * sizeof(wchar_t);
|
||||
HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, bytes);
|
||||
if (!memory) return false;
|
||||
void* destination = GlobalLock(memory);
|
||||
if (!destination) {
|
||||
GlobalFree(memory);
|
||||
return false;
|
||||
}
|
||||
std::memcpy(destination, text.c_str(), bytes);
|
||||
GlobalUnlock(memory);
|
||||
|
||||
bool opened = false;
|
||||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||
if (OpenClipboard(owner)) {
|
||||
opened = true;
|
||||
break;
|
||||
}
|
||||
Sleep(10 + attempt * 4);
|
||||
}
|
||||
if (!opened) {
|
||||
GlobalFree(memory);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool transferred = false;
|
||||
if (EmptyClipboard() && SetClipboardData(CF_UNICODETEXT, memory)) {
|
||||
memory = nullptr;
|
||||
transferred = true;
|
||||
}
|
||||
CloseClipboard();
|
||||
if (memory) GlobalFree(memory);
|
||||
return transferred;
|
||||
}
|
||||
|
||||
void showTrayMenu(HWND hwnd) {
|
||||
POINT cursor{};
|
||||
GetCursorPos(&cursor);
|
||||
|
||||
HMENU menu = CreatePopupMenu();
|
||||
AppendMenuW(menu, MF_STRING, kMenuCapture, L"Сделать снимок\tPrintScreen");
|
||||
AppendMenuW(menu, MF_STRING, kMenuGif, gif_recorder::active()
|
||||
? L"Остановить GIF\tCtrl+PrintScreen"
|
||||
: L"Записать GIF\tCtrl+PrintScreen");
|
||||
AppendMenuW(menu, MF_STRING, kMenuSettings, L"Настройки...");
|
||||
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
|
||||
AppendMenuW(menu, MF_STRING, kMenuExit, L"Выход");
|
||||
|
||||
SetForegroundWindow(hwnd);
|
||||
const UINT command = TrackPopupMenu(
|
||||
menu, TPM_RETURNCMD | TPM_RIGHTBUTTON | TPM_NONOTIFY,
|
||||
cursor.x, cursor.y, 0, hwnd, nullptr);
|
||||
DestroyMenu(menu);
|
||||
if (command) PostMessageW(hwnd, WM_COMMAND, command, 0);
|
||||
}
|
||||
|
||||
void beginCapture(HWND owner, OverlayMode mode = OverlayMode::Screenshot) {
|
||||
if (gif_recorder::active()) {
|
||||
notifyIfEnabled(owner, L"NightShot",
|
||||
L"Сначала останови текущую GIF-запись.", NIIF_WARNING);
|
||||
return;
|
||||
}
|
||||
if (g_overlay && IsWindow(g_overlay)) {
|
||||
SetForegroundWindow(g_overlay);
|
||||
return;
|
||||
}
|
||||
|
||||
ScreenCapture capture = captureVirtualDesktop();
|
||||
if (!capture.valid()) {
|
||||
MessageBoxW(owner, L"Не удалось захватить виртуальный рабочий стол.",
|
||||
L"NightShot", MB_ICONERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
g_overlay = OverlayWindow::create(
|
||||
GetModuleHandleW(nullptr), std::move(capture), owner, mode);
|
||||
if (!g_overlay) {
|
||||
MessageBoxW(owner, L"Не удалось создать окно редактора.",
|
||||
L"NightShot", MB_ICONERROR);
|
||||
}
|
||||
}
|
||||
|
||||
void handleUploadFinished(HWND hwnd, uploader::Result* rawResult) {
|
||||
std::unique_ptr<uploader::Result> result(rawResult);
|
||||
if (!result) return;
|
||||
|
||||
if (!result->success) {
|
||||
std::wstring message = result->message.empty()
|
||||
? L"Неизвестная ошибка загрузки."
|
||||
: result->message;
|
||||
MessageBoxW(hwnd, message.c_str(), L"NightShot — загрузка",
|
||||
MB_OK | MB_ICONERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
const settings::AppSettings configuration = settings::load();
|
||||
if (!result->responseUrl.empty()) {
|
||||
if (configuration.copyUploadedUrl) {
|
||||
copyTextToClipboard(hwnd, result->responseUrl);
|
||||
}
|
||||
if (configuration.openUploadedUrl) {
|
||||
ShellExecuteW(hwnd, L"open", result->responseUrl.c_str(),
|
||||
nullptr, nullptr, SW_SHOWNORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring body = L"Снимок загружен";
|
||||
if (!result->responseUrl.empty()) body += L"\n" + result->responseUrl;
|
||||
else if (!result->message.empty() && result->message != L"Снимок загружен.") {
|
||||
body += L"\n" + result->message;
|
||||
}
|
||||
notifyIfEnabled(hwnd, L"NightShot", body.c_str());
|
||||
}
|
||||
|
||||
void toggleGifCapture(HWND owner) {
|
||||
if (gif_recorder::active()) {
|
||||
gif_recorder::stop();
|
||||
return;
|
||||
}
|
||||
beginCapture(owner, OverlayMode::GifQuick);
|
||||
}
|
||||
|
||||
void handleGifStart(HWND hwnd, gif_recorder::StartRequest* rawRequest) {
|
||||
std::unique_ptr<gif_recorder::StartRequest> request(rawRequest);
|
||||
if (!request) return;
|
||||
if (!gif_recorder::start(*request)) {
|
||||
MessageBoxW(hwnd, L"Не удалось начать GIF-запись. Возможно, запись уже активна.",
|
||||
L"NightShot — GIF", MB_OK | MB_ICONERROR);
|
||||
return;
|
||||
}
|
||||
notifyIfEnabled(hwnd, L"NightShot",
|
||||
L"GIF-запись началась. Ctrl+PrintScreen — остановить.");
|
||||
}
|
||||
|
||||
void handleGifFinished(HWND hwnd, gif_recorder::Result* rawResult) {
|
||||
std::unique_ptr<gif_recorder::Result> result(rawResult);
|
||||
if (!result) {
|
||||
gif_recorder::releaseFinished();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result->success) {
|
||||
const std::wstring message = result->message.empty()
|
||||
? L"Неизвестная ошибка GIF-записи." : result->message;
|
||||
MessageBoxW(hwnd, message.c_str(), L"NightShot — GIF",
|
||||
MB_OK | MB_ICONERROR);
|
||||
} else {
|
||||
std::filesystem::path path(result->path);
|
||||
std::wstring body = path.filename().wstring() + L" сохранён";
|
||||
if (!result->message.empty()) body += L"\n" + result->message;
|
||||
notifyIfEnabled(hwnd, L"NightShot — GIF", body.c_str());
|
||||
}
|
||||
gif_recorder::releaseFinished();
|
||||
}
|
||||
|
||||
void handleScreenshotSaved(HWND hwnd, std::wstring* rawPath) {
|
||||
std::unique_ptr<std::wstring> path(rawPath);
|
||||
if (!path || path->empty()) return;
|
||||
const std::wstring fileName = std::filesystem::path(*path).filename().wstring();
|
||||
notifyIfEnabled(hwnd, L"NightShot",
|
||||
(fileName + L" сохранён").c_str());
|
||||
}
|
||||
|
||||
LRESULT CALLBACK keyboardProc(int code, WPARAM wParam, LPARAM lParam) {
|
||||
if (code == HC_ACTION) {
|
||||
const auto* key = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
|
||||
if (key->vkCode == VK_SNAPSHOT && !(key->flags & LLKHF_INJECTED)) {
|
||||
if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP) {
|
||||
g_printScreenDown = false;
|
||||
return 1;
|
||||
}
|
||||
if ((wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) &&
|
||||
!g_printScreenDown) {
|
||||
g_printScreenDown = true;
|
||||
const bool control =
|
||||
(GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
|
||||
if (g_mainWindow) {
|
||||
PostMessageW(g_mainWindow,
|
||||
control ? kToggleGifCapture : kStartCapture,
|
||||
0, 0);
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return CallNextHookEx(g_keyboardHook, code, wParam, lParam);
|
||||
}
|
||||
|
||||
LRESULT CALLBACK mainWindowProc(HWND hwnd, UINT message,
|
||||
WPARAM wParam, LPARAM lParam) {
|
||||
if (message == g_taskbarCreated && g_taskbarCreated != 0) {
|
||||
addTrayIcon(hwnd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
switch (message) {
|
||||
case WM_CREATE:
|
||||
addTrayIcon(hwnd);
|
||||
return 0;
|
||||
case WM_HOTKEY:
|
||||
if (wParam == kHotkeyScreenshotId) beginCapture(hwnd);
|
||||
else if (wParam == kHotkeyGifId) toggleGifCapture(hwnd);
|
||||
return 0;
|
||||
case kStartCapture:
|
||||
beginCapture(hwnd);
|
||||
return 0;
|
||||
case kToggleGifCapture:
|
||||
toggleGifCapture(hwnd);
|
||||
return 0;
|
||||
case WM_NIGHTSHOT_SCREENSHOT_COPIED:
|
||||
notifyIfEnabled(hwnd, L"NightShot",
|
||||
L"Снимок экрана скопирован в буфер обмена");
|
||||
return 0;
|
||||
case WM_NIGHTSHOT_OPEN_SETTINGS:
|
||||
settings_window::show(hwnd);
|
||||
return 0;
|
||||
case WM_NIGHTSHOT_SETTINGS_CHANGED:
|
||||
return 0;
|
||||
case WM_NIGHTSHOT_UPLOAD_FINISHED:
|
||||
handleUploadFinished(
|
||||
hwnd, reinterpret_cast<uploader::Result*>(lParam));
|
||||
return 0;
|
||||
case WM_NIGHTSHOT_BEGIN_GIF:
|
||||
handleGifStart(
|
||||
hwnd, reinterpret_cast<gif_recorder::StartRequest*>(lParam));
|
||||
return 0;
|
||||
case WM_NIGHTSHOT_GIF_FINISHED:
|
||||
handleGifFinished(
|
||||
hwnd, reinterpret_cast<gif_recorder::Result*>(lParam));
|
||||
return 0;
|
||||
case WM_NIGHTSHOT_SCREENSHOT_SAVED:
|
||||
handleScreenshotSaved(
|
||||
hwnd, reinterpret_cast<std::wstring*>(lParam));
|
||||
return 0;
|
||||
case kTrayMessage:
|
||||
switch (LOWORD(lParam)) {
|
||||
case WM_LBUTTONDBLCLK:
|
||||
case NIN_SELECT:
|
||||
beginCapture(hwnd);
|
||||
break;
|
||||
case WM_CONTEXTMENU:
|
||||
case WM_RBUTTONUP:
|
||||
showTrayMenu(hwnd);
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
case WM_COMMAND:
|
||||
switch (LOWORD(wParam)) {
|
||||
case kMenuCapture:
|
||||
beginCapture(hwnd);
|
||||
return 0;
|
||||
case kMenuGif:
|
||||
toggleGifCapture(hwnd);
|
||||
return 0;
|
||||
case kMenuSettings:
|
||||
settings_window::show(hwnd);
|
||||
return 0;
|
||||
case kMenuExit:
|
||||
DestroyWindow(hwnd);
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_DESTROY:
|
||||
if (settings_window::window() &&
|
||||
IsWindow(settings_window::window())) {
|
||||
DestroyWindow(settings_window::window());
|
||||
}
|
||||
if (g_hotkeysRegistered) {
|
||||
UnregisterHotKey(hwnd, kHotkeyScreenshotId);
|
||||
UnregisterHotKey(hwnd, kHotkeyGifId);
|
||||
g_hotkeysRegistered = false;
|
||||
}
|
||||
gif_recorder::shutdown();
|
||||
removeTrayIcon(hwnd);
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProcW(hwnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
void enablePerMonitorDpi() {
|
||||
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int) {
|
||||
enablePerMonitorDpi();
|
||||
|
||||
// Normal startup is deliberately asInvoker first. It asks for UAC once,
|
||||
// starts the elevated tray process, and exits. Silent Admin starts this
|
||||
// executable elevated through Task Scheduler and skips the prompt.
|
||||
if (!startup::ensureElevated(kMainClass)) return 0;
|
||||
|
||||
HANDLE mutex = CreateMutexW(nullptr, TRUE, kMutexName);
|
||||
if (!mutex) return 1;
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
||||
CloseHandle(mutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const HRESULT comResult = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
if (FAILED(comResult)) {
|
||||
CloseHandle(mutex);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const bool firstRun = !settings::exists();
|
||||
const settings::AppSettings initialSettings = settings::load();
|
||||
if (firstRun) {
|
||||
if (!settings::save(initialSettings)) {
|
||||
MessageBoxW(nullptr, L"Не удалось создать settings.ini в AppData.",
|
||||
L"NightShot — настройки", MB_OK | MB_ICONWARNING);
|
||||
}
|
||||
std::wstring startupError;
|
||||
if (!startup::applyAutostart(initialSettings.startWithWindows,
|
||||
initialSettings.silentAdmin,
|
||||
&startupError)) {
|
||||
MessageBoxW(nullptr, startupError.c_str(),
|
||||
L"NightShot — автозагрузка", MB_OK | MB_ICONWARNING);
|
||||
}
|
||||
}
|
||||
|
||||
[[maybe_unused]] const bool notificationRegistrationReady =
|
||||
notifications::initialize();
|
||||
|
||||
Gdiplus::GdiplusStartupInput gdiplusInput;
|
||||
ULONG_PTR gdiplusToken = 0;
|
||||
if (Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusInput, nullptr) !=
|
||||
Gdiplus::Ok) {
|
||||
notifications::shutdown();
|
||||
CoUninitialize();
|
||||
CloseHandle(mutex);
|
||||
return 3;
|
||||
}
|
||||
|
||||
WNDCLASSEXW wc{};
|
||||
wc.cbSize = sizeof(wc);
|
||||
wc.lpfnWndProc = mainWindowProc;
|
||||
wc.hInstance = instance;
|
||||
wc.hIcon = static_cast<HICON>(LoadImageW(
|
||||
instance, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON,
|
||||
0, 0, LR_DEFAULTSIZE));
|
||||
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||
wc.lpszClassName = kMainClass;
|
||||
if (!RegisterClassExW(&wc)) {
|
||||
Gdiplus::GdiplusShutdown(gdiplusToken);
|
||||
notifications::shutdown();
|
||||
CoUninitialize();
|
||||
CloseHandle(mutex);
|
||||
return 4;
|
||||
}
|
||||
|
||||
g_taskbarCreated = RegisterWindowMessageW(L"TaskbarCreated");
|
||||
g_mainWindow = CreateWindowExW(
|
||||
0, kMainClass, L"NightShot", WS_OVERLAPPED,
|
||||
0, 0, 0, 0, nullptr, nullptr, instance, nullptr);
|
||||
if (!g_mainWindow) {
|
||||
Gdiplus::GdiplusShutdown(gdiplusToken);
|
||||
notifications::shutdown();
|
||||
CoUninitialize();
|
||||
CloseHandle(mutex);
|
||||
return 5;
|
||||
}
|
||||
|
||||
// The low-level hook can distinguish PrintScreen from Ctrl+PrintScreen and
|
||||
// also works when Windows has reserved PrintScreen for Snipping Tool.
|
||||
g_keyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL, keyboardProc, instance, 0);
|
||||
if (!g_keyboardHook) {
|
||||
const BOOL screenshot = RegisterHotKey(
|
||||
g_mainWindow, kHotkeyScreenshotId, MOD_NOREPEAT, VK_SNAPSHOT);
|
||||
const BOOL gif = RegisterHotKey(
|
||||
g_mainWindow, kHotkeyGifId, MOD_CONTROL | MOD_NOREPEAT, VK_SNAPSHOT);
|
||||
g_hotkeysRegistered = screenshot || gif;
|
||||
}
|
||||
|
||||
MSG message{};
|
||||
while (GetMessageW(&message, nullptr, 0, 0) > 0) {
|
||||
if (!settings_window::handleDialogMessage(&message)) {
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
}
|
||||
if (g_overlay && !IsWindow(g_overlay)) g_overlay = nullptr;
|
||||
}
|
||||
|
||||
if (g_keyboardHook) UnhookWindowsHookEx(g_keyboardHook);
|
||||
g_keyboardHook = nullptr;
|
||||
g_mainWindow = nullptr;
|
||||
Gdiplus::GdiplusShutdown(gdiplusToken);
|
||||
notifications::shutdown();
|
||||
CoUninitialize();
|
||||
ReleaseMutex(mutex);
|
||||
CloseHandle(mutex);
|
||||
return static_cast<int>(message.wParam);
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
#include "notifications.hpp"
|
||||
|
||||
#include <roapi.h>
|
||||
#include <shlobj.h>
|
||||
#include <shobjidl.h>
|
||||
#include <windows.data.xml.dom.h>
|
||||
#include <windows.ui.notifications.h>
|
||||
#include <wrl/client.h>
|
||||
#include <wrl/wrappers/corewrappers.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace notifications {
|
||||
namespace {
|
||||
using Microsoft::WRL::ComPtr;
|
||||
using Microsoft::WRL::Wrappers::HStringReference;
|
||||
using ABI::Windows::Data::Xml::Dom::IXmlDocument;
|
||||
using ABI::Windows::Data::Xml::Dom::IXmlNode;
|
||||
using ABI::Windows::Data::Xml::Dom::IXmlNodeList;
|
||||
using ABI::Windows::Data::Xml::Dom::IXmlText;
|
||||
using ABI::Windows::UI::Notifications::IToastNotification;
|
||||
using ABI::Windows::UI::Notifications::IToastNotificationFactory;
|
||||
using ABI::Windows::UI::Notifications::IToastNotificationManagerStatics;
|
||||
using ABI::Windows::UI::Notifications::IToastNotifier;
|
||||
|
||||
constexpr wchar_t kAppUserModelId[] = L"NightSync.NightShot";
|
||||
constexpr wchar_t kShortcutName[] = L"NightShot.lnk";
|
||||
constexpr wchar_t kLegacyShortcutName[] = L"SnapLight.lnk";
|
||||
constexpr wchar_t kToastTitle[] = L"NightShot";
|
||||
constexpr wchar_t kToastBody[] = L"Снимок экрана скопирован в буфер обмена";
|
||||
constexpr PROPERTYKEY kAppUserModelIdKey = {
|
||||
{0x9F4C2855, 0x9F79, 0x4B39, {0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3}},
|
||||
5};
|
||||
|
||||
bool g_runtimeInitialized = false;
|
||||
bool g_shortcutReady = false;
|
||||
|
||||
std::wstring executablePath() {
|
||||
std::vector<wchar_t> buffer(32768, L'\0');
|
||||
const DWORD length = GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
|
||||
if (length == 0 || length >= buffer.size()) return {};
|
||||
return std::wstring(buffer.data(), length);
|
||||
}
|
||||
|
||||
std::wstring parentDirectory(const std::wstring& path) {
|
||||
const std::wstring::size_type slash = path.find_last_of(L"\\/");
|
||||
return slash == std::wstring::npos ? std::wstring() : path.substr(0, slash);
|
||||
}
|
||||
|
||||
HRESULT installOrRefreshShortcut() {
|
||||
PWSTR programsPathRaw = nullptr;
|
||||
HRESULT hr = SHGetKnownFolderPath(FOLDERID_Programs, KF_FLAG_CREATE, nullptr, &programsPathRaw);
|
||||
if (FAILED(hr) || !programsPathRaw) return FAILED(hr) ? hr : E_FAIL;
|
||||
|
||||
std::wstring programsPath(programsPathRaw);
|
||||
CoTaskMemFree(programsPathRaw);
|
||||
if (!programsPath.empty() && programsPath.back() != L'\\') programsPath.push_back(L'\\');
|
||||
const std::wstring shortcutPath = programsPath + kShortcutName;
|
||||
const std::wstring legacyShortcutPath = programsPath + kLegacyShortcutName;
|
||||
|
||||
const std::wstring exePath = executablePath();
|
||||
if (exePath.empty()) return HRESULT_FROM_WIN32(GetLastError());
|
||||
|
||||
ComPtr<IShellLinkW> shellLink;
|
||||
hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
|
||||
IID_PPV_ARGS(shellLink.ReleaseAndGetAddressOf()));
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
hr = shellLink->SetPath(exePath.c_str());
|
||||
if (FAILED(hr)) return hr;
|
||||
hr = shellLink->SetArguments(L"");
|
||||
if (FAILED(hr)) return hr;
|
||||
hr = shellLink->SetDescription(L"NightShot screenshot utility");
|
||||
if (FAILED(hr)) return hr;
|
||||
const std::wstring workingDirectory = parentDirectory(exePath);
|
||||
if (!workingDirectory.empty()) {
|
||||
hr = shellLink->SetWorkingDirectory(workingDirectory.c_str());
|
||||
if (FAILED(hr)) return hr;
|
||||
}
|
||||
shellLink->SetIconLocation(exePath.c_str(), 0);
|
||||
|
||||
ComPtr<IPropertyStore> propertyStore;
|
||||
hr = shellLink.As(&propertyStore);
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
std::wstring mutableAppId(kAppUserModelId);
|
||||
PROPVARIANT appIdValue{};
|
||||
appIdValue.vt = VT_LPWSTR;
|
||||
appIdValue.pwszVal = mutableAppId.data();
|
||||
hr = propertyStore->SetValue(kAppUserModelIdKey, appIdValue);
|
||||
if (FAILED(hr)) return hr;
|
||||
hr = propertyStore->Commit();
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
ComPtr<IPersistFile> persistFile;
|
||||
hr = shellLink.As(&persistFile);
|
||||
if (FAILED(hr)) return hr;
|
||||
hr = persistFile->Save(shortcutPath.c_str(), TRUE);
|
||||
if (SUCCEEDED(hr)) {
|
||||
DeleteFileW(legacyShortcutPath.c_str());
|
||||
SHChangeNotify(SHCNE_DELETE, SHCNF_PATHW, legacyShortcutPath.c_str(), nullptr);
|
||||
SHChangeNotify(SHCNE_UPDATEITEM, SHCNF_PATHW, shortcutPath.c_str(), nullptr);
|
||||
}
|
||||
return hr;
|
||||
}
|
||||
|
||||
HRESULT appendText(IXmlDocument* document, UINT32 index, const wchar_t* value) {
|
||||
if (!document || !value) return E_INVALIDARG;
|
||||
|
||||
ComPtr<IXmlNodeList> textNodes;
|
||||
HRESULT hr = document->GetElementsByTagName(HStringReference(L"text").Get(),
|
||||
textNodes.ReleaseAndGetAddressOf());
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
UINT32 count = 0;
|
||||
hr = textNodes->get_Length(&count);
|
||||
if (FAILED(hr) || index >= count) return FAILED(hr) ? hr : E_INVALIDARG;
|
||||
|
||||
ComPtr<IXmlNode> targetNode;
|
||||
hr = textNodes->Item(index, targetNode.ReleaseAndGetAddressOf());
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
ComPtr<IXmlText> textNode;
|
||||
hr = document->CreateTextNode(HStringReference(value).Get(), textNode.ReleaseAndGetAddressOf());
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
ComPtr<IXmlNode> textAsNode;
|
||||
hr = textNode.As(&textAsNode);
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
ComPtr<IXmlNode> appendedNode;
|
||||
return targetNode->AppendChild(textAsNode.Get(), appendedNode.ReleaseAndGetAddressOf());
|
||||
}
|
||||
|
||||
HRESULT sendToast(const wchar_t* title, const wchar_t* body) {
|
||||
ComPtr<IToastNotificationManagerStatics> toastManager;
|
||||
HRESULT hr = RoGetActivationFactory(
|
||||
HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(),
|
||||
IID_PPV_ARGS(toastManager.ReleaseAndGetAddressOf()));
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
ComPtr<IXmlDocument> toastXml;
|
||||
hr = toastManager->GetTemplateContent(ABI::Windows::UI::Notifications::ToastTemplateType_ToastText02,
|
||||
toastXml.ReleaseAndGetAddressOf());
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
hr = appendText(toastXml.Get(), 0, title ? title : kToastTitle);
|
||||
if (FAILED(hr)) return hr;
|
||||
hr = appendText(toastXml.Get(), 1, body ? body : L"");
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
ComPtr<IToastNotificationFactory> toastFactory;
|
||||
hr = RoGetActivationFactory(
|
||||
HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification).Get(),
|
||||
IID_PPV_ARGS(toastFactory.ReleaseAndGetAddressOf()));
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
ComPtr<IToastNotification> toast;
|
||||
hr = toastFactory->CreateToastNotification(toastXml.Get(), toast.ReleaseAndGetAddressOf());
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
ComPtr<IToastNotifier> notifier;
|
||||
hr = toastManager->CreateToastNotifierWithId(HStringReference(kAppUserModelId).Get(),
|
||||
notifier.ReleaseAndGetAddressOf());
|
||||
if (FAILED(hr)) return hr;
|
||||
|
||||
return notifier->Show(toast.Get());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool initialize() {
|
||||
const HRESULT appIdResult = SetCurrentProcessExplicitAppUserModelID(kAppUserModelId);
|
||||
|
||||
const HRESULT runtimeResult = RoInitialize(RO_INIT_SINGLETHREADED);
|
||||
g_runtimeInitialized = SUCCEEDED(runtimeResult);
|
||||
|
||||
g_shortcutReady = SUCCEEDED(installOrRefreshShortcut());
|
||||
return SUCCEEDED(appIdResult) && g_runtimeInitialized && g_shortcutReady;
|
||||
}
|
||||
|
||||
bool show(const wchar_t* title, const wchar_t* body) {
|
||||
if (!g_runtimeInitialized || !g_shortcutReady) return false;
|
||||
return SUCCEEDED(sendToast(title, body));
|
||||
}
|
||||
|
||||
bool showScreenshotCopied() {
|
||||
return show(kToastTitle, kToastBody);
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
if (g_runtimeInitialized) {
|
||||
RoUninitialize();
|
||||
g_runtimeInitialized = false;
|
||||
}
|
||||
g_shortcutReady = false;
|
||||
}
|
||||
|
||||
} // namespace notifications
|
||||
+3371
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,275 @@
|
||||
#include "settings.hpp"
|
||||
|
||||
#include <shlobj.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cwctype>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
namespace settings {
|
||||
namespace {
|
||||
|
||||
std::wstring trim(std::wstring value) {
|
||||
const auto isSpace = [](wchar_t ch) {
|
||||
return std::iswspace(static_cast<wint_t>(ch)) != 0;
|
||||
};
|
||||
value.erase(value.begin(), std::find_if_not(value.begin(), value.end(), isSpace));
|
||||
value.erase(std::find_if_not(value.rbegin(), value.rend(), isSpace).base(), value.end());
|
||||
return value;
|
||||
}
|
||||
|
||||
std::wstring readString(const wchar_t* section, const wchar_t* key,
|
||||
const wchar_t* fallback, const std::wstring& path) {
|
||||
std::vector<wchar_t> buffer(8192, L'\0');
|
||||
const DWORD length = GetPrivateProfileStringW(
|
||||
section, key, fallback, buffer.data(), static_cast<DWORD>(buffer.size()), path.c_str());
|
||||
return std::wstring(buffer.data(), length);
|
||||
}
|
||||
|
||||
bool readBool(const wchar_t* section, const wchar_t* key, bool fallback,
|
||||
const std::wstring& path) {
|
||||
return GetPrivateProfileIntW(section, key, fallback ? 1 : 0, path.c_str()) != 0;
|
||||
}
|
||||
|
||||
bool writeBool(const wchar_t* section, const wchar_t* key, bool value,
|
||||
const std::wstring& path) {
|
||||
return WritePrivateProfileStringW(section, key, value ? L"1" : L"0", path.c_str()) != FALSE;
|
||||
}
|
||||
|
||||
int readInt(const wchar_t* section, const wchar_t* key, int fallback,
|
||||
const std::wstring& path, int minimum, int maximum) {
|
||||
const int value = static_cast<int>(GetPrivateProfileIntW(section, key, fallback, path.c_str()));
|
||||
return std::clamp(value, minimum, maximum);
|
||||
}
|
||||
|
||||
bool writeInt(const wchar_t* section, const wchar_t* key, int value,
|
||||
const std::wstring& path) {
|
||||
const std::wstring text = std::to_wstring(value);
|
||||
return WritePrivateProfileStringW(section, key, text.c_str(), path.c_str()) != FALSE;
|
||||
}
|
||||
|
||||
std::wstring paletteKey(std::size_t index) {
|
||||
std::wostringstream stream;
|
||||
stream << L"Slot" << std::setfill(L'0') << std::setw(2) << index;
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::wstring argbText(std::uint32_t argb) {
|
||||
std::wostringstream stream;
|
||||
stream << std::uppercase << std::hex << std::setfill(L'0') << std::setw(8) << argb;
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
bool parseArgb(const std::wstring& text, std::uint32_t& result) {
|
||||
if (text.size() != 8) return false;
|
||||
wchar_t* end = nullptr;
|
||||
const unsigned long value = std::wcstoul(text.c_str(), &end, 16);
|
||||
if (!end || *end != L'\0') return false;
|
||||
result = static_cast<std::uint32_t>(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
void applyDefaultPalette(AppSettings& result) {
|
||||
constexpr std::array<std::uint32_t, 8> defaults{
|
||||
0xFF3B82F6u, 0xFF8B5CF6u, 0xFFEC4A5Fu, 0xFFFF9125u,
|
||||
0xFFFFD541u, 0xFF48CC79u, 0xFFF5F5F5u, 0xFF191919u};
|
||||
for (std::size_t i = 0; i < defaults.size(); ++i) {
|
||||
result.paletteArgb[i] = defaults[i];
|
||||
result.paletteOccupied[i] = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path roamingSettingsDirectory(const wchar_t* productName) {
|
||||
PWSTR roamingRaw = nullptr;
|
||||
const HRESULT result = SHGetKnownFolderPath(
|
||||
FOLDERID_RoamingAppData, KF_FLAG_CREATE, nullptr, &roamingRaw);
|
||||
if (FAILED(result) || !roamingRaw) return {};
|
||||
|
||||
std::filesystem::path directory(roamingRaw);
|
||||
CoTaskMemFree(roamingRaw);
|
||||
directory /= L"NightSync";
|
||||
directory /= productName;
|
||||
return directory;
|
||||
}
|
||||
|
||||
bool isRegularFile(const std::filesystem::path& path) {
|
||||
const DWORD attributes = GetFileAttributesW(path.c_str());
|
||||
return attributes != INVALID_FILE_ATTRIBUTES &&
|
||||
(attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
|
||||
}
|
||||
|
||||
void migrateLegacySettings(const std::wstring& destination) {
|
||||
const std::filesystem::path destinationPath(destination);
|
||||
if (isRegularFile(destinationPath)) return;
|
||||
|
||||
std::filesystem::path legacyDirectory = roamingSettingsDirectory(L"SnapLight");
|
||||
if (legacyDirectory.empty()) return;
|
||||
const std::filesystem::path legacyPath = legacyDirectory / L"settings.ini";
|
||||
if (!isRegularFile(legacyPath)) return;
|
||||
|
||||
std::error_code ignored;
|
||||
std::filesystem::create_directories(destinationPath.parent_path(), ignored);
|
||||
CopyFileW(legacyPath.c_str(), destinationPath.c_str(), TRUE);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::wstring defaultSaveDirectory() {
|
||||
PWSTR picturesRaw = nullptr;
|
||||
const HRESULT result = SHGetKnownFolderPath(
|
||||
FOLDERID_Pictures, KF_FLAG_CREATE, nullptr, &picturesRaw);
|
||||
if (SUCCEEDED(result) && picturesRaw) {
|
||||
std::filesystem::path directory(picturesRaw);
|
||||
CoTaskMemFree(picturesRaw);
|
||||
directory /= L"NightShot";
|
||||
return directory.wstring();
|
||||
}
|
||||
|
||||
wchar_t fallback[MAX_PATH]{};
|
||||
const DWORD length = GetEnvironmentVariableW(L"USERPROFILE", fallback, MAX_PATH);
|
||||
std::filesystem::path directory;
|
||||
if (length > 0 && length < MAX_PATH) directory = fallback;
|
||||
else directory = L".";
|
||||
directory /= L"Pictures";
|
||||
directory /= L"NightShot";
|
||||
return directory.wstring();
|
||||
}
|
||||
|
||||
std::wstring filePath() {
|
||||
std::filesystem::path directory = roamingSettingsDirectory(L"NightShot");
|
||||
if (directory.empty()) return L"NightShot.ini";
|
||||
|
||||
std::error_code ignored;
|
||||
std::filesystem::create_directories(directory, ignored);
|
||||
directory /= L"settings.ini";
|
||||
return directory.wstring();
|
||||
}
|
||||
|
||||
bool exists() {
|
||||
const DWORD attributes = GetFileAttributesW(filePath().c_str());
|
||||
return attributes != INVALID_FILE_ATTRIBUTES &&
|
||||
(attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
|
||||
}
|
||||
|
||||
std::wstring normalizeEndpoint(std::wstring value) {
|
||||
value = trim(std::move(value));
|
||||
if (value.empty()) return value;
|
||||
if (value.find(L"://") == std::wstring::npos) value.insert(0, L"https://");
|
||||
return value;
|
||||
}
|
||||
|
||||
AppSettings load() {
|
||||
const std::wstring path = filePath();
|
||||
migrateLegacySettings(path);
|
||||
AppSettings result;
|
||||
|
||||
result.notificationsEnabled = readBool(L"General", L"Notifications", true, path);
|
||||
result.startWithWindows = readBool(L"General", L"StartWithWindows", true, path);
|
||||
result.silentAdmin = readBool(L"General", L"SilentAdmin", false, path);
|
||||
|
||||
result.copyUploadedUrl = readBool(L"Upload", L"CopyUrl", true, path);
|
||||
result.openUploadedUrl = readBool(L"Upload", L"OpenUrl", false, path);
|
||||
result.closeEditorAfterUpload = readBool(L"Upload", L"CloseEditor", true, path);
|
||||
result.uploadUrl = normalizeEndpoint(readString(L"Upload", L"Url", L"", path));
|
||||
result.uploadName = trim(readString(L"Upload", L"Name", L"Screenshot1", path));
|
||||
result.multipartField = trim(readString(L"Upload", L"MultipartField", L"file", path));
|
||||
|
||||
result.saveDirectory = trim(readString(
|
||||
L"Saving", L"Directory", defaultSaveDirectory().c_str(), path));
|
||||
if (result.saveDirectory.empty()) result.saveDirectory = defaultSaveDirectory();
|
||||
result.screenshotFormat = readInt(L"Saving", L"ScreenshotFormat", 0, path, 0, 1) == 1
|
||||
? ScreenshotFormat::Jpeg : ScreenshotFormat::Png;
|
||||
result.jpegQuality = readInt(L"Saving", L"JpegQuality", 92, path, 1, 100);
|
||||
|
||||
const int loadedFps = readInt(L"GIF", L"Fps", 30, path, 15, 60);
|
||||
constexpr std::array<int, 4> allowedFps{15, 30, 45, 60};
|
||||
result.gifFps = *std::min_element(
|
||||
allowedFps.begin(), allowedFps.end(),
|
||||
[loadedFps](int a, int b) { return std::abs(a - loadedFps) < std::abs(b - loadedFps); });
|
||||
result.gifQuality = readInt(L"GIF", L"Quality", 8, path, 1, 10);
|
||||
result.gifMaxDurationSeconds = readInt(L"GIF", L"MaxDurationSeconds", 30, path, 1, 3600);
|
||||
result.gifCaptureCursor = readBool(L"GIF", L"CaptureCursor", true, path);
|
||||
|
||||
result.penWidth = readInt(L"Drawing", L"PenWidth", 3, path, 1, 64);
|
||||
result.lineWidth = readInt(L"Drawing", L"LineWidth", 3, path, 1, 64);
|
||||
result.arrowWidth = readInt(L"Drawing", L"ArrowWidth", 3, path, 1, 64);
|
||||
result.rectangleWidth = readInt(L"Drawing", L"RectangleWidth", 3, path, 1, 64);
|
||||
result.markerWidth = readInt(L"Drawing", L"MarkerWidth", 15, path, 1, 96);
|
||||
|
||||
applyDefaultPalette(result);
|
||||
for (std::size_t i = 0; i < kSavedColorCount; ++i) {
|
||||
const std::wstring key = paletteKey(i);
|
||||
const std::wstring value = trim(readString(L"Palette", key.c_str(), L"", path));
|
||||
if (value.empty() || value == L"-") {
|
||||
if (i >= 8) result.paletteOccupied[i] = false;
|
||||
continue;
|
||||
}
|
||||
std::uint32_t argb = 0;
|
||||
if (parseArgb(value, argb)) {
|
||||
result.paletteArgb[i] = argb;
|
||||
result.paletteOccupied[i] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.uploadName.empty()) result.uploadName = L"Screenshot1";
|
||||
if (result.multipartField.empty()) result.multipartField = L"file";
|
||||
if (!result.startWithWindows) result.silentAdmin = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool save(const AppSettings& value) {
|
||||
const std::wstring path = filePath();
|
||||
const std::wstring endpoint = normalizeEndpoint(value.uploadUrl);
|
||||
const std::wstring trimmedName = trim(value.uploadName);
|
||||
const std::wstring trimmedField = trim(value.multipartField);
|
||||
const std::wstring name = trimmedName.empty() ? L"Screenshot1" : trimmedName;
|
||||
const std::wstring field = trimmedField.empty() ? L"file" : trimmedField;
|
||||
|
||||
bool ok = true;
|
||||
ok = writeBool(L"General", L"Notifications", value.notificationsEnabled, path) && ok;
|
||||
ok = writeBool(L"General", L"StartWithWindows", value.startWithWindows, path) && ok;
|
||||
ok = writeBool(L"General", L"SilentAdmin",
|
||||
value.startWithWindows && value.silentAdmin, path) && ok;
|
||||
|
||||
ok = writeBool(L"Upload", L"CopyUrl", value.copyUploadedUrl, path) && ok;
|
||||
ok = writeBool(L"Upload", L"OpenUrl", value.openUploadedUrl, path) && ok;
|
||||
ok = writeBool(L"Upload", L"CloseEditor", value.closeEditorAfterUpload, path) && ok;
|
||||
ok = WritePrivateProfileStringW(L"Upload", L"Url", endpoint.c_str(), path.c_str()) != FALSE && ok;
|
||||
ok = WritePrivateProfileStringW(L"Upload", L"Name", name.c_str(), path.c_str()) != FALSE && ok;
|
||||
ok = WritePrivateProfileStringW(L"Upload", L"MultipartField", field.c_str(), path.c_str()) != FALSE && ok;
|
||||
|
||||
const std::wstring directory = trim(value.saveDirectory).empty()
|
||||
? defaultSaveDirectory() : trim(value.saveDirectory);
|
||||
ok = WritePrivateProfileStringW(L"Saving", L"Directory", directory.c_str(), path.c_str()) != FALSE && ok;
|
||||
ok = writeInt(L"Saving", L"ScreenshotFormat",
|
||||
value.screenshotFormat == ScreenshotFormat::Jpeg ? 1 : 0, path) && ok;
|
||||
ok = writeInt(L"Saving", L"JpegQuality", std::clamp(value.jpegQuality, 1, 100), path) && ok;
|
||||
|
||||
const int fps = value.gifFps == 15 || value.gifFps == 45 || value.gifFps == 60
|
||||
? value.gifFps : 30;
|
||||
ok = writeInt(L"GIF", L"Fps", fps, path) && ok;
|
||||
ok = writeInt(L"GIF", L"Quality", std::clamp(value.gifQuality, 1, 10), path) && ok;
|
||||
ok = writeInt(L"GIF", L"MaxDurationSeconds",
|
||||
std::clamp(value.gifMaxDurationSeconds, 1, 3600), path) && ok;
|
||||
ok = writeBool(L"GIF", L"CaptureCursor", value.gifCaptureCursor, path) && ok;
|
||||
|
||||
ok = writeInt(L"Drawing", L"PenWidth", std::clamp(value.penWidth, 1, 64), path) && ok;
|
||||
ok = writeInt(L"Drawing", L"LineWidth", std::clamp(value.lineWidth, 1, 64), path) && ok;
|
||||
ok = writeInt(L"Drawing", L"ArrowWidth", std::clamp(value.arrowWidth, 1, 64), path) && ok;
|
||||
ok = writeInt(L"Drawing", L"RectangleWidth", std::clamp(value.rectangleWidth, 1, 64), path) && ok;
|
||||
ok = writeInt(L"Drawing", L"MarkerWidth", std::clamp(value.markerWidth, 1, 96), path) && ok;
|
||||
|
||||
for (std::size_t i = 0; i < kSavedColorCount; ++i) {
|
||||
const std::wstring key = paletteKey(i);
|
||||
const std::wstring slot = value.paletteOccupied[i] ? argbText(value.paletteArgb[i]) : L"-";
|
||||
ok = WritePrivateProfileStringW(L"Palette", key.c_str(), slot.c_str(), path.c_str()) != FALSE && ok;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace settings
|
||||
@@ -0,0 +1,660 @@
|
||||
#include "settings_window.hpp"
|
||||
|
||||
#include "app_messages.hpp"
|
||||
#include "resource.h"
|
||||
#include "settings.hpp"
|
||||
#include "startup.hpp"
|
||||
|
||||
#include <shellapi.h>
|
||||
#include <shlobj.h>
|
||||
#include <winhttp.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cwchar>
|
||||
#include <iterator>
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace settings_window {
|
||||
namespace {
|
||||
constexpr wchar_t kClassName[] = L"NightShotSettingsWindow";
|
||||
|
||||
constexpr int kIdNotifications = 2001;
|
||||
constexpr int kIdAutostart = 2002;
|
||||
constexpr int kIdSilentAdmin = 2003;
|
||||
constexpr int kIdUploadUrl = 2004;
|
||||
constexpr int kIdUploadName = 2005;
|
||||
constexpr int kIdMultipartField = 2006;
|
||||
constexpr int kIdCopyUrl = 2007;
|
||||
constexpr int kIdOpenUrl = 2008;
|
||||
constexpr int kIdCloseEditor = 2009;
|
||||
constexpr int kIdSave = 2010;
|
||||
constexpr int kIdCancel = 2011;
|
||||
constexpr int kIdSaveDirectory = 2012;
|
||||
constexpr int kIdBrowseDirectory = 2013;
|
||||
constexpr int kIdOpenDirectory = 2014;
|
||||
constexpr int kIdScreenshotFormat = 2015;
|
||||
constexpr int kIdJpegQuality = 2016;
|
||||
constexpr int kIdGifFps = 2017;
|
||||
constexpr int kIdGifQuality = 2018;
|
||||
constexpr int kIdGifLimit = 2019;
|
||||
constexpr int kIdGifCursor = 2020;
|
||||
|
||||
struct WindowState {
|
||||
HWND window = nullptr;
|
||||
HWND owner = nullptr;
|
||||
HFONT font = nullptr;
|
||||
UINT dpi = 96;
|
||||
|
||||
HWND generalGroup = nullptr;
|
||||
HWND notifications = nullptr;
|
||||
HWND autostart = nullptr;
|
||||
HWND silentAdmin = nullptr;
|
||||
HWND privilegeStatus = nullptr;
|
||||
|
||||
HWND savingGroup = nullptr;
|
||||
HWND directoryLabel = nullptr;
|
||||
HWND saveDirectory = nullptr;
|
||||
HWND browseDirectory = nullptr;
|
||||
HWND openDirectory = nullptr;
|
||||
HWND formatLabel = nullptr;
|
||||
HWND screenshotFormat = nullptr;
|
||||
HWND jpegQualityLabel = nullptr;
|
||||
HWND jpegQuality = nullptr;
|
||||
HWND gifFpsLabel = nullptr;
|
||||
HWND gifFps = nullptr;
|
||||
HWND gifQualityLabel = nullptr;
|
||||
HWND gifQuality = nullptr;
|
||||
HWND gifLimitLabel = nullptr;
|
||||
HWND gifLimit = nullptr;
|
||||
HWND gifLimitHint = nullptr;
|
||||
HWND gifCursor = nullptr;
|
||||
|
||||
HWND uploadGroup = nullptr;
|
||||
HWND urlLabel = nullptr;
|
||||
HWND uploadUrl = nullptr;
|
||||
HWND urlHint = nullptr;
|
||||
HWND nameLabel = nullptr;
|
||||
HWND uploadName = nullptr;
|
||||
HWND fieldLabel = nullptr;
|
||||
HWND multipartField = nullptr;
|
||||
HWND copyUrl = nullptr;
|
||||
HWND openUrl = nullptr;
|
||||
HWND closeEditor = nullptr;
|
||||
|
||||
HWND save = nullptr;
|
||||
HWND cancel = nullptr;
|
||||
};
|
||||
|
||||
HWND g_window = nullptr;
|
||||
ATOM g_class = 0;
|
||||
|
||||
int px(UINT dpi, int value) {
|
||||
return MulDiv(value, static_cast<int>(dpi), 96);
|
||||
}
|
||||
|
||||
void setFont(HWND control, HFONT font) {
|
||||
if (control) SendMessageW(control, WM_SETFONT, reinterpret_cast<WPARAM>(font), TRUE);
|
||||
}
|
||||
|
||||
HWND createControl(WindowState& state, const wchar_t* className,
|
||||
const wchar_t* text, DWORD style, int id,
|
||||
DWORD exStyle = 0) {
|
||||
HWND control = CreateWindowExW(
|
||||
exStyle, className, text, WS_CHILD | WS_VISIBLE | style,
|
||||
0, 0, 0, 0, state.window,
|
||||
reinterpret_cast<HMENU>(static_cast<INT_PTR>(id)),
|
||||
GetModuleHandleW(nullptr), nullptr);
|
||||
setFont(control, state.font);
|
||||
return control;
|
||||
}
|
||||
|
||||
std::array<HWND, 34> allControls(const WindowState& state) {
|
||||
return {
|
||||
state.generalGroup, state.notifications, state.autostart,
|
||||
state.silentAdmin, state.privilegeStatus,
|
||||
state.savingGroup, state.directoryLabel, state.saveDirectory,
|
||||
state.browseDirectory, state.openDirectory, state.formatLabel,
|
||||
state.screenshotFormat, state.jpegQualityLabel, state.jpegQuality,
|
||||
state.gifFpsLabel, state.gifFps, state.gifQualityLabel,
|
||||
state.gifQuality, state.gifLimitLabel, state.gifLimit,
|
||||
state.gifLimitHint, state.gifCursor,
|
||||
state.uploadGroup, state.urlLabel, state.uploadUrl, state.urlHint,
|
||||
state.nameLabel, state.uploadName, state.fieldLabel,
|
||||
state.multipartField, state.copyUrl, state.openUrl,
|
||||
state.closeEditor, state.save
|
||||
};
|
||||
}
|
||||
|
||||
void replaceFont(WindowState& state, UINT dpi) {
|
||||
state.dpi = dpi ? dpi : 96;
|
||||
if (state.font) DeleteObject(state.font);
|
||||
state.font = CreateFontW(
|
||||
-MulDiv(9, static_cast<int>(state.dpi), 72), 0, 0, 0, FW_NORMAL,
|
||||
FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS,
|
||||
CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
|
||||
DEFAULT_PITCH | FF_DONTCARE, L"Segoe UI");
|
||||
for (HWND control : allControls(state)) setFont(control, state.font);
|
||||
setFont(state.cancel, state.font);
|
||||
}
|
||||
|
||||
void layout(WindowState& state, int width, int height) {
|
||||
const int margin = px(state.dpi, 16);
|
||||
const int gap = px(state.dpi, 9);
|
||||
const int row = px(state.dpi, 24);
|
||||
const int editHeight = px(state.dpi, 27);
|
||||
const int buttonWidth = px(state.dpi, 108);
|
||||
const int buttonHeight = px(state.dpi, 31);
|
||||
const int contentWidth = std::max(px(state.dpi, 520), width - margin * 2);
|
||||
const int innerLeft = margin + px(state.dpi, 14);
|
||||
const int innerWidth = contentWidth - px(state.dpi, 28);
|
||||
|
||||
int y = margin;
|
||||
const int generalHeight = px(state.dpi, 142);
|
||||
MoveWindow(state.generalGroup, margin, y, contentWidth, generalHeight, TRUE);
|
||||
int gy = y + px(state.dpi, 24);
|
||||
MoveWindow(state.notifications, innerLeft, gy, innerWidth, row, TRUE);
|
||||
gy += row + px(state.dpi, 3);
|
||||
MoveWindow(state.autostart, innerLeft, gy, innerWidth, row, TRUE);
|
||||
gy += row + px(state.dpi, 3);
|
||||
MoveWindow(state.silentAdmin, innerLeft + px(state.dpi, 20), gy,
|
||||
innerWidth - px(state.dpi, 20), row, TRUE);
|
||||
gy += row + px(state.dpi, 1);
|
||||
MoveWindow(state.privilegeStatus, innerLeft + px(state.dpi, 20), gy,
|
||||
innerWidth - px(state.dpi, 20), px(state.dpi, 32), TRUE);
|
||||
|
||||
y += generalHeight + gap;
|
||||
const int savingHeight = px(state.dpi, 246);
|
||||
MoveWindow(state.savingGroup, margin, y, contentWidth, savingHeight, TRUE);
|
||||
|
||||
const int labelWidth = px(state.dpi, 145);
|
||||
int sy = y + px(state.dpi, 27);
|
||||
MoveWindow(state.directoryLabel, innerLeft, sy + px(state.dpi, 4),
|
||||
labelWidth, row, TRUE);
|
||||
const int smallButton = px(state.dpi, 86);
|
||||
const int directoryLeft = innerLeft + labelWidth;
|
||||
const int directoryWidth = innerWidth - labelWidth - smallButton * 2 - px(state.dpi, 12);
|
||||
MoveWindow(state.saveDirectory, directoryLeft, sy, directoryWidth, editHeight, TRUE);
|
||||
MoveWindow(state.browseDirectory, directoryLeft + directoryWidth + px(state.dpi, 4), sy,
|
||||
smallButton, editHeight, TRUE);
|
||||
MoveWindow(state.openDirectory, directoryLeft + directoryWidth + smallButton + px(state.dpi, 8), sy,
|
||||
smallButton, editHeight, TRUE);
|
||||
sy += editHeight + px(state.dpi, 10);
|
||||
|
||||
const int comboWidth = px(state.dpi, 132);
|
||||
MoveWindow(state.formatLabel, innerLeft, sy + px(state.dpi, 4), labelWidth, row, TRUE);
|
||||
MoveWindow(state.screenshotFormat, directoryLeft, sy, comboWidth, px(state.dpi, 160), TRUE);
|
||||
MoveWindow(state.jpegQualityLabel, directoryLeft + comboWidth + px(state.dpi, 18),
|
||||
sy + px(state.dpi, 4), px(state.dpi, 110), row, TRUE);
|
||||
MoveWindow(state.jpegQuality, directoryLeft + comboWidth + px(state.dpi, 128),
|
||||
sy, px(state.dpi, 62), editHeight, TRUE);
|
||||
sy += editHeight + px(state.dpi, 10);
|
||||
|
||||
MoveWindow(state.gifFpsLabel, innerLeft, sy + px(state.dpi, 4), labelWidth, row, TRUE);
|
||||
MoveWindow(state.gifFps, directoryLeft, sy, comboWidth, px(state.dpi, 150), TRUE);
|
||||
MoveWindow(state.gifQualityLabel, directoryLeft + comboWidth + px(state.dpi, 18),
|
||||
sy + px(state.dpi, 4), px(state.dpi, 110), row, TRUE);
|
||||
MoveWindow(state.gifQuality, directoryLeft + comboWidth + px(state.dpi, 128),
|
||||
sy, px(state.dpi, 88), px(state.dpi, 220), TRUE);
|
||||
sy += editHeight + px(state.dpi, 10);
|
||||
|
||||
MoveWindow(state.gifLimitLabel, innerLeft, sy + px(state.dpi, 4), labelWidth, row, TRUE);
|
||||
MoveWindow(state.gifLimit, directoryLeft, sy, px(state.dpi, 82), editHeight, TRUE);
|
||||
MoveWindow(state.gifLimitHint, directoryLeft + px(state.dpi, 92), sy + px(state.dpi, 4),
|
||||
innerWidth - labelWidth - px(state.dpi, 92), row, TRUE);
|
||||
sy += editHeight + px(state.dpi, 8);
|
||||
MoveWindow(state.gifCursor, innerLeft, sy, innerWidth, row, TRUE);
|
||||
|
||||
y += savingHeight + gap;
|
||||
const int buttonsY = height - margin - buttonHeight;
|
||||
const int uploadBottom = buttonsY - gap;
|
||||
const int uploadHeight = std::max(px(state.dpi, 235), uploadBottom - y);
|
||||
MoveWindow(state.uploadGroup, margin, y, contentWidth, uploadHeight, TRUE);
|
||||
|
||||
const int uploadLabelWidth = px(state.dpi, 126);
|
||||
const int uploadEditLeft = innerLeft + uploadLabelWidth;
|
||||
const int uploadEditWidth = innerWidth - uploadLabelWidth;
|
||||
int uy = y + px(state.dpi, 27);
|
||||
MoveWindow(state.urlLabel, innerLeft, uy + px(state.dpi, 4), uploadLabelWidth, row, TRUE);
|
||||
MoveWindow(state.uploadUrl, uploadEditLeft, uy, uploadEditWidth, editHeight, TRUE);
|
||||
uy += editHeight + px(state.dpi, 4);
|
||||
MoveWindow(state.urlHint, uploadEditLeft, uy, uploadEditWidth, px(state.dpi, 34), TRUE);
|
||||
uy += px(state.dpi, 42);
|
||||
|
||||
MoveWindow(state.nameLabel, innerLeft, uy + px(state.dpi, 4), uploadLabelWidth, row, TRUE);
|
||||
MoveWindow(state.uploadName, uploadEditLeft, uy, uploadEditWidth, editHeight, TRUE);
|
||||
uy += editHeight + px(state.dpi, 7);
|
||||
MoveWindow(state.fieldLabel, innerLeft, uy + px(state.dpi, 4), uploadLabelWidth, row, TRUE);
|
||||
MoveWindow(state.multipartField, uploadEditLeft, uy, uploadEditWidth, editHeight, TRUE);
|
||||
uy += editHeight + px(state.dpi, 9);
|
||||
MoveWindow(state.copyUrl, innerLeft, uy, innerWidth, row, TRUE);
|
||||
uy += row + px(state.dpi, 1);
|
||||
MoveWindow(state.openUrl, innerLeft, uy, innerWidth, row, TRUE);
|
||||
uy += row + px(state.dpi, 1);
|
||||
MoveWindow(state.closeEditor, innerLeft, uy, innerWidth, row, TRUE);
|
||||
|
||||
MoveWindow(state.cancel, width - margin - buttonWidth, buttonsY,
|
||||
buttonWidth, buttonHeight, TRUE);
|
||||
MoveWindow(state.save, width - margin * 2 - buttonWidth * 2, buttonsY,
|
||||
buttonWidth, buttonHeight, TRUE);
|
||||
}
|
||||
|
||||
void setCheck(HWND control, bool checked) {
|
||||
SendMessageW(control, BM_SETCHECK, checked ? BST_CHECKED : BST_UNCHECKED, 0);
|
||||
}
|
||||
|
||||
bool getCheck(HWND control) {
|
||||
return SendMessageW(control, BM_GETCHECK, 0, 0) == BST_CHECKED;
|
||||
}
|
||||
|
||||
std::wstring getText(HWND control) {
|
||||
const int length = GetWindowTextLengthW(control);
|
||||
std::vector<wchar_t> buffer(static_cast<std::size_t>(std::max(0, length)) + 1, L'\0');
|
||||
GetWindowTextW(control, buffer.data(), static_cast<int>(buffer.size()));
|
||||
return std::wstring(buffer.data());
|
||||
}
|
||||
|
||||
void addComboValue(HWND combo, const std::wstring& label, int value) {
|
||||
const LRESULT index = SendMessageW(combo, CB_ADDSTRING, 0,
|
||||
reinterpret_cast<LPARAM>(label.c_str()));
|
||||
if (index >= 0) SendMessageW(combo, CB_SETITEMDATA, index, value);
|
||||
}
|
||||
|
||||
void selectComboValue(HWND combo, int value) {
|
||||
const int count = static_cast<int>(SendMessageW(combo, CB_GETCOUNT, 0, 0));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (static_cast<int>(SendMessageW(combo, CB_GETITEMDATA, i, 0)) == value) {
|
||||
SendMessageW(combo, CB_SETCURSEL, i, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
SendMessageW(combo, CB_SETCURSEL, 0, 0);
|
||||
}
|
||||
|
||||
int comboValue(HWND combo, int fallback) {
|
||||
const LRESULT index = SendMessageW(combo, CB_GETCURSEL, 0, 0);
|
||||
if (index == CB_ERR) return fallback;
|
||||
const LRESULT value = SendMessageW(combo, CB_GETITEMDATA, index, 0);
|
||||
return value == CB_ERR ? fallback : static_cast<int>(value);
|
||||
}
|
||||
|
||||
bool parseInteger(HWND control, int minimum, int maximum, int& result) {
|
||||
const std::wstring text = getText(control);
|
||||
wchar_t* end = nullptr;
|
||||
const long value = std::wcstol(text.c_str(), &end, 10);
|
||||
if (!end || *end != L'\0' || value < minimum || value > maximum) return false;
|
||||
result = static_cast<int>(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
void updateAutostartControls(WindowState& state) {
|
||||
const bool enabled = getCheck(state.autostart);
|
||||
EnableWindow(state.silentAdmin, enabled ? TRUE : FALSE);
|
||||
if (!enabled) setCheck(state.silentAdmin, false);
|
||||
const wchar_t* text = enabled && getCheck(state.silentAdmin)
|
||||
? L"Silent Admin: вход в Windows без UAC через Планировщик."
|
||||
: L"Обычный режим: при входе в Windows один UAC; во время работы повторных запросов нет.";
|
||||
SetWindowTextW(state.privilegeStatus, text);
|
||||
}
|
||||
|
||||
void updateFormatControls(WindowState& state) {
|
||||
const bool jpeg = comboValue(state.screenshotFormat, 0) == 1;
|
||||
EnableWindow(state.jpegQualityLabel, jpeg ? TRUE : FALSE);
|
||||
EnableWindow(state.jpegQuality, jpeg ? TRUE : FALSE);
|
||||
}
|
||||
|
||||
void populate(WindowState& state) {
|
||||
const settings::AppSettings current = settings::load();
|
||||
setCheck(state.notifications, current.notificationsEnabled);
|
||||
setCheck(state.autostart, current.startWithWindows);
|
||||
setCheck(state.silentAdmin, current.silentAdmin);
|
||||
setCheck(state.copyUrl, current.copyUploadedUrl);
|
||||
setCheck(state.openUrl, current.openUploadedUrl);
|
||||
setCheck(state.closeEditor, current.closeEditorAfterUpload);
|
||||
setCheck(state.gifCursor, current.gifCaptureCursor);
|
||||
SetWindowTextW(state.uploadUrl, current.uploadUrl.c_str());
|
||||
SetWindowTextW(state.uploadName, current.uploadName.c_str());
|
||||
SetWindowTextW(state.multipartField, current.multipartField.c_str());
|
||||
SetWindowTextW(state.saveDirectory, current.saveDirectory.c_str());
|
||||
SetWindowTextW(state.jpegQuality, std::to_wstring(current.jpegQuality).c_str());
|
||||
SetWindowTextW(state.gifLimit, std::to_wstring(current.gifMaxDurationSeconds).c_str());
|
||||
selectComboValue(state.screenshotFormat,
|
||||
current.screenshotFormat == settings::ScreenshotFormat::Jpeg ? 1 : 0);
|
||||
selectComboValue(state.gifFps, current.gifFps);
|
||||
selectComboValue(state.gifQuality, current.gifQuality);
|
||||
updateAutostartControls(state);
|
||||
updateFormatControls(state);
|
||||
}
|
||||
|
||||
bool isValidUploadUrl(const std::wstring& url) {
|
||||
if (url.empty()) return true;
|
||||
URL_COMPONENTSW components{};
|
||||
components.dwStructSize = sizeof(components);
|
||||
components.dwSchemeLength = static_cast<DWORD>(-1);
|
||||
components.dwHostNameLength = static_cast<DWORD>(-1);
|
||||
components.dwUrlPathLength = static_cast<DWORD>(-1);
|
||||
components.dwExtraInfoLength = static_cast<DWORD>(-1);
|
||||
return WinHttpCrackUrl(url.c_str(), static_cast<DWORD>(url.size()), 0,
|
||||
&components) != FALSE &&
|
||||
components.dwHostNameLength > 0 &&
|
||||
(components.nScheme == INTERNET_SCHEME_HTTP ||
|
||||
components.nScheme == INTERNET_SCHEME_HTTPS);
|
||||
}
|
||||
|
||||
int CALLBACK browseCallback(HWND hwnd, UINT message, LPARAM, LPARAM data) {
|
||||
if (message == BFFM_INITIALIZED && data) {
|
||||
SendMessageW(hwnd, BFFM_SETSELECTIONW, TRUE, data);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::wstring chooseFolder(HWND owner, const std::wstring& initial) {
|
||||
BROWSEINFOW info{};
|
||||
info.hwndOwner = owner;
|
||||
info.lpszTitle = L"Выбери папку для снимков и GIF";
|
||||
info.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE | BIF_USENEWUI;
|
||||
info.lpfn = browseCallback;
|
||||
info.lParam = reinterpret_cast<LPARAM>(initial.c_str());
|
||||
LPITEMIDLIST selected = SHBrowseForFolderW(&info);
|
||||
if (!selected) return {};
|
||||
wchar_t path[MAX_PATH]{};
|
||||
const bool ok = SHGetPathFromIDListW(selected, path) != FALSE;
|
||||
CoTaskMemFree(selected);
|
||||
return ok ? std::wstring(path) : std::wstring();
|
||||
}
|
||||
|
||||
bool saveValues(WindowState& state) {
|
||||
settings::AppSettings next = settings::load();
|
||||
next.notificationsEnabled = getCheck(state.notifications);
|
||||
next.startWithWindows = getCheck(state.autostart);
|
||||
next.silentAdmin = next.startWithWindows && getCheck(state.silentAdmin);
|
||||
next.copyUploadedUrl = getCheck(state.copyUrl);
|
||||
next.openUploadedUrl = getCheck(state.openUrl);
|
||||
next.closeEditorAfterUpload = getCheck(state.closeEditor);
|
||||
next.uploadUrl = settings::normalizeEndpoint(getText(state.uploadUrl));
|
||||
next.uploadName = getText(state.uploadName);
|
||||
next.multipartField = getText(state.multipartField);
|
||||
next.saveDirectory = getText(state.saveDirectory);
|
||||
next.screenshotFormat = comboValue(state.screenshotFormat, 0) == 1
|
||||
? settings::ScreenshotFormat::Jpeg : settings::ScreenshotFormat::Png;
|
||||
next.gifFps = comboValue(state.gifFps, 30);
|
||||
next.gifQuality = comboValue(state.gifQuality, 8);
|
||||
next.gifCaptureCursor = getCheck(state.gifCursor);
|
||||
|
||||
if (!isValidUploadUrl(next.uploadUrl)) {
|
||||
MessageBoxW(g_window,
|
||||
L"URL загрузчика должен быть обычным http:// или https:// адресом.",
|
||||
L"NightShot — настройки", MB_OK | MB_ICONWARNING);
|
||||
SetFocus(state.uploadUrl);
|
||||
return false;
|
||||
}
|
||||
if (next.saveDirectory.empty()) next.saveDirectory = settings::defaultSaveDirectory();
|
||||
std::error_code directoryError;
|
||||
std::filesystem::create_directories(next.saveDirectory, directoryError);
|
||||
if (directoryError) {
|
||||
MessageBoxW(g_window, L"Не удалось создать выбранную папку сохранения.",
|
||||
L"NightShot — настройки", MB_OK | MB_ICONWARNING);
|
||||
SetFocus(state.saveDirectory);
|
||||
return false;
|
||||
}
|
||||
if (!parseInteger(state.jpegQuality, 1, 100, next.jpegQuality)) {
|
||||
MessageBoxW(g_window, L"Качество JPEG должно быть от 1 до 100.",
|
||||
L"NightShot — настройки", MB_OK | MB_ICONWARNING);
|
||||
SetFocus(state.jpegQuality);
|
||||
return false;
|
||||
}
|
||||
if (!parseInteger(state.gifLimit, 1, 3600, next.gifMaxDurationSeconds)) {
|
||||
MessageBoxW(g_window, L"Лимит GIF должен быть от 1 до 3600 секунд.",
|
||||
L"NightShot — настройки", MB_OK | MB_ICONWARNING);
|
||||
SetFocus(state.gifLimit);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring startupError;
|
||||
if (!startup::applyAutostart(next.startWithWindows, next.silentAdmin,
|
||||
&startupError)) {
|
||||
MessageBoxW(g_window, startupError.c_str(),
|
||||
L"Не удалось изменить автозагрузку", MB_OK | MB_ICONERROR);
|
||||
return false;
|
||||
}
|
||||
if (!settings::save(next)) {
|
||||
MessageBoxW(g_window, L"Не удалось сохранить settings.ini в AppData.",
|
||||
L"NightShot — настройки", MB_OK | MB_ICONERROR);
|
||||
return false;
|
||||
}
|
||||
if (state.owner && IsWindow(state.owner)) {
|
||||
PostMessageW(state.owner, WM_NIGHTSHOT_SETTINGS_CHANGED, 0, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK windowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
|
||||
auto* state = reinterpret_cast<WindowState*>(
|
||||
GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||
if (message == WM_NCCREATE) {
|
||||
const auto* create = reinterpret_cast<const CREATESTRUCTW*>(lParam);
|
||||
state = reinterpret_cast<WindowState*>(create->lpCreateParams);
|
||||
if (state) state->window = hwnd;
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(state));
|
||||
}
|
||||
if (!state) return DefWindowProcW(hwnd, message, wParam, lParam);
|
||||
|
||||
switch (message) {
|
||||
case WM_CREATE: {
|
||||
state->dpi = GetDpiForWindow(hwnd);
|
||||
replaceFont(*state, state->dpi);
|
||||
|
||||
state->generalGroup = createControl(*state, L"BUTTON", L"Общие", BS_GROUPBOX, 0);
|
||||
state->notifications = createControl(*state, L"BUTTON",
|
||||
L"Показывать системные уведомления Windows",
|
||||
BS_AUTOCHECKBOX | WS_TABSTOP, kIdNotifications);
|
||||
state->autostart = createControl(*state, L"BUTTON", L"Запускать вместе с Windows",
|
||||
BS_AUTOCHECKBOX | WS_TABSTOP, kIdAutostart);
|
||||
state->silentAdmin = createControl(*state, L"BUTTON",
|
||||
L"Silent Admin — запуск без UAC",
|
||||
BS_AUTOCHECKBOX | WS_TABSTOP, kIdSilentAdmin);
|
||||
state->privilegeStatus = createControl(*state, L"STATIC", L"", SS_LEFT, 0);
|
||||
|
||||
state->savingGroup = createControl(*state, L"BUTTON", L"Сохранение и GIF", BS_GROUPBOX, 0);
|
||||
state->directoryLabel = createControl(*state, L"STATIC", L"Папка сохранения:", SS_LEFT, 0);
|
||||
state->saveDirectory = createControl(*state, L"EDIT", L"",
|
||||
WS_TABSTOP | ES_AUTOHSCROLL, kIdSaveDirectory, WS_EX_CLIENTEDGE);
|
||||
state->browseDirectory = createControl(*state, L"BUTTON", L"Обзор...",
|
||||
BS_PUSHBUTTON | WS_TABSTOP, kIdBrowseDirectory);
|
||||
state->openDirectory = createControl(*state, L"BUTTON", L"Открыть",
|
||||
BS_PUSHBUTTON | WS_TABSTOP, kIdOpenDirectory);
|
||||
state->formatLabel = createControl(*state, L"STATIC", L"Формат снимков:", SS_LEFT, 0);
|
||||
state->screenshotFormat = createControl(*state, L"COMBOBOX", L"",
|
||||
CBS_DROPDOWNLIST | WS_TABSTOP, kIdScreenshotFormat);
|
||||
addComboValue(state->screenshotFormat, L"PNG", 0);
|
||||
addComboValue(state->screenshotFormat, L"JPEG", 1);
|
||||
state->jpegQualityLabel = createControl(*state, L"STATIC", L"JPEG качество:", SS_LEFT, 0);
|
||||
state->jpegQuality = createControl(*state, L"EDIT", L"92",
|
||||
ES_NUMBER | ES_AUTOHSCROLL | WS_TABSTOP, kIdJpegQuality, WS_EX_CLIENTEDGE);
|
||||
state->gifFpsLabel = createControl(*state, L"STATIC", L"GIF по умолчанию:", SS_LEFT, 0);
|
||||
state->gifFps = createControl(*state, L"COMBOBOX", L"",
|
||||
CBS_DROPDOWNLIST | WS_TABSTOP, kIdGifFps);
|
||||
for (int fps : {15, 30, 45, 60}) addComboValue(state->gifFps, std::to_wstring(fps) + L" FPS", fps);
|
||||
state->gifQualityLabel = createControl(*state, L"STATIC", L"GIF качество:", SS_LEFT, 0);
|
||||
state->gifQuality = createControl(*state, L"COMBOBOX", L"",
|
||||
CBS_DROPDOWNLIST | WS_TABSTOP | WS_VSCROLL, kIdGifQuality);
|
||||
for (int quality = 1; quality <= 10; ++quality) {
|
||||
addComboValue(state->gifQuality, std::to_wstring(quality) + L" / 10", quality);
|
||||
}
|
||||
state->gifLimitLabel = createControl(*state, L"STATIC", L"Лимит GIF, сек:", SS_LEFT, 0);
|
||||
state->gifLimit = createControl(*state, L"EDIT", L"30",
|
||||
ES_NUMBER | ES_AUTOHSCROLL | WS_TABSTOP, kIdGifLimit, WS_EX_CLIENTEDGE);
|
||||
state->gifLimitHint = createControl(*state, L"STATIC",
|
||||
L"Запись остановится автоматически при достижении лимита.", SS_LEFT, 0);
|
||||
state->gifCursor = createControl(*state, L"BUTTON", L"Записывать курсор в GIF",
|
||||
BS_AUTOCHECKBOX | WS_TABSTOP, kIdGifCursor);
|
||||
|
||||
state->uploadGroup = createControl(*state, L"BUTTON", L"API-загрузка", BS_GROUPBOX, 0);
|
||||
state->urlLabel = createControl(*state, L"STATIC", L"URL сервера:", SS_LEFT, 0);
|
||||
state->uploadUrl = createControl(*state, L"EDIT", L"",
|
||||
WS_TABSTOP | ES_AUTOHSCROLL, kIdUploadUrl, WS_EX_CLIENTEDGE);
|
||||
state->urlHint = createControl(*state, L"STATIC",
|
||||
L"К URL добавляется параметр name, PNG отправляется как multipart/form-data.", SS_LEFT, 0);
|
||||
state->nameLabel = createControl(*state, L"STATIC", L"Параметр name:", SS_LEFT, 0);
|
||||
state->uploadName = createControl(*state, L"EDIT", L"",
|
||||
WS_TABSTOP | ES_AUTOHSCROLL, kIdUploadName, WS_EX_CLIENTEDGE);
|
||||
state->fieldLabel = createControl(*state, L"STATIC", L"Multipart-поле:", SS_LEFT, 0);
|
||||
state->multipartField = createControl(*state, L"EDIT", L"",
|
||||
WS_TABSTOP | ES_AUTOHSCROLL, kIdMultipartField, WS_EX_CLIENTEDGE);
|
||||
state->copyUrl = createControl(*state, L"BUTTON", L"Копировать URL из ответа сервера",
|
||||
BS_AUTOCHECKBOX | WS_TABSTOP, kIdCopyUrl);
|
||||
state->openUrl = createControl(*state, L"BUTTON", L"Открывать полученный URL в браузере",
|
||||
BS_AUTOCHECKBOX | WS_TABSTOP, kIdOpenUrl);
|
||||
state->closeEditor = createControl(*state, L"BUTTON", L"Закрывать редактор после начала загрузки",
|
||||
BS_AUTOCHECKBOX | WS_TABSTOP, kIdCloseEditor);
|
||||
|
||||
state->save = createControl(*state, L"BUTTON", L"Сохранить",
|
||||
BS_DEFPUSHBUTTON | WS_TABSTOP, kIdSave);
|
||||
state->cancel = createControl(*state, L"BUTTON", L"Отмена",
|
||||
BS_PUSHBUTTON | WS_TABSTOP, kIdCancel);
|
||||
|
||||
populate(*state);
|
||||
RECT client{};
|
||||
GetClientRect(hwnd, &client);
|
||||
layout(*state, client.right, client.bottom);
|
||||
return 0;
|
||||
}
|
||||
case WM_SIZE:
|
||||
layout(*state, LOWORD(lParam), HIWORD(lParam));
|
||||
return 0;
|
||||
case WM_DPICHANGED: {
|
||||
replaceFont(*state, HIWORD(wParam));
|
||||
const RECT* suggested = reinterpret_cast<const RECT*>(lParam);
|
||||
SetWindowPos(hwnd, nullptr, suggested->left, suggested->top,
|
||||
suggested->right - suggested->left,
|
||||
suggested->bottom - suggested->top,
|
||||
SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
RECT client{};
|
||||
GetClientRect(hwnd, &client);
|
||||
layout(*state, client.right, client.bottom);
|
||||
return 0;
|
||||
}
|
||||
case WM_GETMINMAXINFO: {
|
||||
auto* info = reinterpret_cast<MINMAXINFO*>(lParam);
|
||||
info->ptMinTrackSize.x = px(state->dpi, 700);
|
||||
info->ptMinTrackSize.y = px(state->dpi, 755);
|
||||
return 0;
|
||||
}
|
||||
case WM_COMMAND:
|
||||
switch (LOWORD(wParam)) {
|
||||
case kIdAutostart:
|
||||
case kIdSilentAdmin:
|
||||
updateAutostartControls(*state);
|
||||
return 0;
|
||||
case kIdScreenshotFormat:
|
||||
if (HIWORD(wParam) == CBN_SELCHANGE) updateFormatControls(*state);
|
||||
return 0;
|
||||
case kIdBrowseDirectory: {
|
||||
const std::wstring selected = chooseFolder(hwnd, getText(state->saveDirectory));
|
||||
if (!selected.empty()) SetWindowTextW(state->saveDirectory, selected.c_str());
|
||||
return 0;
|
||||
}
|
||||
case kIdOpenDirectory: {
|
||||
std::wstring directory = getText(state->saveDirectory);
|
||||
if (directory.empty()) directory = settings::defaultSaveDirectory();
|
||||
std::error_code ignored;
|
||||
std::filesystem::create_directories(directory, ignored);
|
||||
ShellExecuteW(hwnd, L"open", directory.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||||
return 0;
|
||||
}
|
||||
case kIdSave:
|
||||
if (saveValues(*state)) DestroyWindow(hwnd);
|
||||
return 0;
|
||||
case kIdCancel:
|
||||
DestroyWindow(hwnd);
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_CLOSE:
|
||||
DestroyWindow(hwnd);
|
||||
return 0;
|
||||
case WM_DESTROY:
|
||||
if (state->font) DeleteObject(state->font);
|
||||
state->font = nullptr;
|
||||
g_window = nullptr;
|
||||
if (state->owner && IsWindow(state->owner)) EnableWindow(state->owner, TRUE);
|
||||
delete state;
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProcW(hwnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
void ensureClass() {
|
||||
if (g_class) return;
|
||||
WNDCLASSEXW wc{};
|
||||
wc.cbSize = sizeof(wc);
|
||||
wc.lpfnWndProc = windowProc;
|
||||
wc.hInstance = GetModuleHandleW(nullptr);
|
||||
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||
wc.hIcon = static_cast<HICON>(LoadImageW(
|
||||
wc.hInstance, MAKEINTRESOURCEW(IDI_APP_ICON), IMAGE_ICON,
|
||||
0, 0, LR_DEFAULTSIZE));
|
||||
if (!wc.hIcon) wc.hIcon = LoadIconW(nullptr, IDI_APPLICATION);
|
||||
wc.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_BTNFACE + 1);
|
||||
wc.lpszClassName = kClassName;
|
||||
g_class = RegisterClassExW(&wc);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void show(HWND owner) {
|
||||
if (g_window && IsWindow(g_window)) {
|
||||
ShowWindow(g_window, SW_RESTORE);
|
||||
SetForegroundWindow(g_window);
|
||||
return;
|
||||
}
|
||||
ensureClass();
|
||||
if (!g_class) return;
|
||||
|
||||
auto* state = new WindowState();
|
||||
state->owner = owner;
|
||||
const UINT dpi = owner ? GetDpiForWindow(owner) : 96;
|
||||
const int width = px(dpi, 760);
|
||||
const int height = px(dpi, 790);
|
||||
|
||||
MONITORINFO info{};
|
||||
info.cbSize = sizeof(info);
|
||||
RECT work{};
|
||||
const HMONITOR monitor = MonitorFromWindow(owner, MONITOR_DEFAULTTONEAREST);
|
||||
if (GetMonitorInfoW(monitor, &info)) work = info.rcWork;
|
||||
else SystemParametersInfoW(SPI_GETWORKAREA, 0, &work, 0);
|
||||
const LONG workWidth = work.right - work.left;
|
||||
const LONG workHeight = work.bottom - work.top;
|
||||
const int x = work.left + std::max<LONG>(0, (workWidth - width) / 2);
|
||||
const int y = work.top + std::max<LONG>(0, (workHeight - height) / 2);
|
||||
|
||||
g_window = CreateWindowExW(
|
||||
WS_EX_CONTROLPARENT | WS_EX_DLGMODALFRAME,
|
||||
kClassName, L"NightShot — настройки",
|
||||
WS_OVERLAPPEDWINDOW,
|
||||
x, y, width, height, owner, nullptr,
|
||||
GetModuleHandleW(nullptr), state);
|
||||
if (!g_window) {
|
||||
delete state;
|
||||
return;
|
||||
}
|
||||
if (owner) EnableWindow(owner, FALSE);
|
||||
ShowWindow(g_window, SW_SHOWNORMAL);
|
||||
UpdateWindow(g_window);
|
||||
SetForegroundWindow(g_window);
|
||||
}
|
||||
|
||||
bool handleDialogMessage(MSG* message) {
|
||||
return message && g_window && IsWindow(g_window) &&
|
||||
IsDialogMessageW(g_window, message) != FALSE;
|
||||
}
|
||||
|
||||
HWND window() {
|
||||
return g_window;
|
||||
}
|
||||
|
||||
} // namespace settings_window
|
||||
+364
@@ -0,0 +1,364 @@
|
||||
#include "startup.hpp"
|
||||
|
||||
#include <sddl.h>
|
||||
#include <shellapi.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cwchar>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace startup {
|
||||
namespace {
|
||||
constexpr wchar_t kRunKey[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
||||
constexpr wchar_t kRunValue[] = L"NightShot";
|
||||
constexpr wchar_t kTaskName[] = L"NightShot Silent Admin";
|
||||
constexpr wchar_t kLegacyRunValue[] = L"SnapLight";
|
||||
constexpr wchar_t kLegacyTaskName[] = L"SnapLight Silent Admin";
|
||||
constexpr wchar_t kLegacyMainWindowClass[] = L"SnapLightMainWindow";
|
||||
|
||||
std::wstring executablePath() {
|
||||
std::vector<wchar_t> buffer(32768, L'\0');
|
||||
const DWORD length = GetModuleFileNameW(nullptr, buffer.data(),
|
||||
static_cast<DWORD>(buffer.size()));
|
||||
if (length == 0 || length >= buffer.size()) return {};
|
||||
return std::wstring(buffer.data(), length);
|
||||
}
|
||||
|
||||
std::wstring parentDirectory(const std::wstring& path) {
|
||||
const std::wstring::size_type slash = path.find_last_of(L"\\/");
|
||||
return slash == std::wstring::npos ? std::wstring() : path.substr(0, slash);
|
||||
}
|
||||
|
||||
std::wstring quote(const std::wstring& value) {
|
||||
return L"\"" + value + L"\"";
|
||||
}
|
||||
|
||||
std::wstring win32Error(DWORD code) {
|
||||
wchar_t* raw = nullptr;
|
||||
const DWORD length = FormatMessageW(
|
||||
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
|
||||
FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr, code, 0, reinterpret_cast<wchar_t*>(&raw), 0, nullptr);
|
||||
std::wstring result = length && raw
|
||||
? std::wstring(raw, length)
|
||||
: L"Ошибка Windows " + std::to_wstring(code);
|
||||
if (raw) LocalFree(raw);
|
||||
while (!result.empty() &&
|
||||
(result.back() == L'\r' || result.back() == L'\n' || result.back() == L' ')) {
|
||||
result.pop_back();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::wstring> commandLineArguments() {
|
||||
int count = 0;
|
||||
LPWSTR* raw = CommandLineToArgvW(GetCommandLineW(), &count);
|
||||
std::vector<std::wstring> result;
|
||||
if (raw) {
|
||||
result.reserve(static_cast<std::size_t>(count));
|
||||
for (int i = 0; i < count; ++i) result.emplace_back(raw[i]);
|
||||
LocalFree(raw);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::wstring elevationArguments() {
|
||||
const std::vector<std::wstring> args = commandLineArguments();
|
||||
std::wstring result;
|
||||
bool hasElevatedMarker = false;
|
||||
for (std::size_t i = 1; i < args.size(); ++i) {
|
||||
if (args[i] == L"--elevated") {
|
||||
hasElevatedMarker = true;
|
||||
continue;
|
||||
}
|
||||
if (!result.empty()) result.push_back(L' ');
|
||||
result += quote(args[i]);
|
||||
}
|
||||
if (!hasElevatedMarker) {
|
||||
if (!result.empty()) result.push_back(L' ');
|
||||
result += L"--elevated";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool setRunValue(bool enabled, std::wstring* error) {
|
||||
HKEY key = nullptr;
|
||||
LONG status = RegCreateKeyExW(HKEY_CURRENT_USER, kRunKey, 0, nullptr, 0,
|
||||
KEY_SET_VALUE, nullptr, &key, nullptr);
|
||||
if (status != ERROR_SUCCESS) {
|
||||
if (error) *error = win32Error(static_cast<DWORD>(status));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
const std::wstring exe = executablePath();
|
||||
if (exe.empty()) {
|
||||
RegCloseKey(key);
|
||||
if (error) *error = L"Не удалось определить путь к NightShot.exe.";
|
||||
return false;
|
||||
}
|
||||
const std::wstring command = quote(exe) + L" --startup";
|
||||
status = RegSetValueExW(
|
||||
key, kRunValue, 0, REG_SZ,
|
||||
reinterpret_cast<const BYTE*>(command.c_str()),
|
||||
static_cast<DWORD>((command.size() + 1) * sizeof(wchar_t)));
|
||||
} else {
|
||||
status = RegDeleteValueW(key, kRunValue);
|
||||
if (status == ERROR_FILE_NOT_FOUND) status = ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
RegCloseKey(key);
|
||||
if (status != ERROR_SUCCESS && error) {
|
||||
*error = win32Error(static_cast<DWORD>(status));
|
||||
}
|
||||
return status == ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
bool deleteRunValue(const wchar_t* valueName) {
|
||||
HKEY key = nullptr;
|
||||
LONG status = RegOpenKeyExW(HKEY_CURRENT_USER, kRunKey, 0, KEY_SET_VALUE, &key);
|
||||
if (status == ERROR_FILE_NOT_FOUND) return true;
|
||||
if (status != ERROR_SUCCESS) return false;
|
||||
status = RegDeleteValueW(key, valueName);
|
||||
RegCloseKey(key);
|
||||
return status == ERROR_SUCCESS || status == ERROR_FILE_NOT_FOUND;
|
||||
}
|
||||
|
||||
std::wstring schtasksPath() {
|
||||
std::vector<wchar_t> buffer(MAX_PATH + 1, L'\0');
|
||||
const UINT length = GetSystemDirectoryW(buffer.data(),
|
||||
static_cast<UINT>(buffer.size()));
|
||||
if (length == 0 || length >= buffer.size()) return L"schtasks.exe";
|
||||
std::wstring result(buffer.data(), length);
|
||||
result += L"\\schtasks.exe";
|
||||
return result;
|
||||
}
|
||||
|
||||
DWORD runHiddenProcess(std::wstring commandLine) {
|
||||
STARTUPINFOW startupInfo{};
|
||||
startupInfo.cb = sizeof(startupInfo);
|
||||
PROCESS_INFORMATION processInfo{};
|
||||
std::vector<wchar_t> mutableCommand(commandLine.begin(), commandLine.end());
|
||||
mutableCommand.push_back(L'\0');
|
||||
|
||||
if (!CreateProcessW(nullptr, mutableCommand.data(), nullptr, nullptr, FALSE,
|
||||
CREATE_NO_WINDOW, nullptr, nullptr, &startupInfo, &processInfo)) {
|
||||
return GetLastError();
|
||||
}
|
||||
|
||||
WaitForSingleObject(processInfo.hProcess, INFINITE);
|
||||
DWORD exitCode = ERROR_GEN_FAILURE;
|
||||
GetExitCodeProcess(processInfo.hProcess, &exitCode);
|
||||
CloseHandle(processInfo.hThread);
|
||||
CloseHandle(processInfo.hProcess);
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
std::wstring currentUserSid() {
|
||||
HANDLE token = nullptr;
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) return {};
|
||||
|
||||
DWORD bytes = 0;
|
||||
GetTokenInformation(token, TokenUser, nullptr, 0, &bytes);
|
||||
if (bytes == 0) {
|
||||
CloseHandle(token);
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<BYTE> storage(bytes);
|
||||
if (!GetTokenInformation(token, TokenUser, storage.data(), bytes, &bytes)) {
|
||||
CloseHandle(token);
|
||||
return {};
|
||||
}
|
||||
CloseHandle(token);
|
||||
|
||||
const auto* user = reinterpret_cast<const TOKEN_USER*>(storage.data());
|
||||
LPWSTR rawSid = nullptr;
|
||||
if (!ConvertSidToStringSidW(user->User.Sid, &rawSid) || !rawSid) return {};
|
||||
std::wstring result(rawSid);
|
||||
LocalFree(rawSid);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::wstring xmlEscape(const std::wstring& value) {
|
||||
std::wstring result;
|
||||
result.reserve(value.size() + 16);
|
||||
for (wchar_t ch : value) {
|
||||
switch (ch) {
|
||||
case L'&': result += L"&"; break;
|
||||
case L'<': result += L"<"; break;
|
||||
case L'>': result += L">"; break;
|
||||
case L'\"': result += L"""; break;
|
||||
case L'\'': result += L"'"; break;
|
||||
default: result.push_back(ch); break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool writeUtf16File(const std::wstring& path, const std::wstring& text,
|
||||
std::wstring* error) {
|
||||
HANDLE file = CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS,
|
||||
FILE_ATTRIBUTE_TEMPORARY, nullptr);
|
||||
if (file == INVALID_HANDLE_VALUE) {
|
||||
if (error) *error = win32Error(GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
const WORD bom = 0xFEFF;
|
||||
DWORD written = 0;
|
||||
bool ok = WriteFile(file, &bom, sizeof(bom), &written, nullptr) != FALSE &&
|
||||
written == sizeof(bom);
|
||||
if (ok) {
|
||||
const DWORD byteCount = static_cast<DWORD>(text.size() * sizeof(wchar_t));
|
||||
written = 0;
|
||||
ok = WriteFile(file, text.data(), byteCount, &written, nullptr) != FALSE &&
|
||||
written == byteCount;
|
||||
}
|
||||
if (!ok && error) *error = win32Error(GetLastError());
|
||||
CloseHandle(file);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool deleteTask(const wchar_t* taskName) {
|
||||
const std::wstring command = quote(schtasksPath()) +
|
||||
L" /Delete /F /TN " + quote(taskName);
|
||||
const DWORD result = runHiddenProcess(command);
|
||||
return result == ERROR_SUCCESS || result == 1;
|
||||
}
|
||||
|
||||
bool createSilentTask(std::wstring* error) {
|
||||
const std::wstring exe = executablePath();
|
||||
const std::wstring sid = currentUserSid();
|
||||
if (exe.empty() || sid.empty()) {
|
||||
if (error) *error = L"Не удалось определить путь приложения или SID пользователя.";
|
||||
return false;
|
||||
}
|
||||
|
||||
wchar_t tempDirectory[MAX_PATH + 1]{};
|
||||
wchar_t tempFile[MAX_PATH + 1]{};
|
||||
if (GetTempPathW(MAX_PATH, tempDirectory) == 0 ||
|
||||
GetTempFileNameW(tempDirectory, L"NSH", 0, tempFile) == 0) {
|
||||
if (error) *error = win32Error(GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::wstring xml =
|
||||
L"<?xml version=\"1.0\" encoding=\"UTF-16\"?>\r\n"
|
||||
L"<Task version=\"1.4\" xmlns=\"http://schemas.microsoft.com/windows/2004/02/mit/task\">\r\n"
|
||||
L" <RegistrationInfo><Description>NightShot silent elevated startup</Description></RegistrationInfo>\r\n"
|
||||
L" <Triggers><LogonTrigger><Enabled>true</Enabled><UserId>" + xmlEscape(sid) +
|
||||
L"</UserId></LogonTrigger></Triggers>\r\n"
|
||||
L" <Principals><Principal id=\"Author\"><UserId>" + xmlEscape(sid) +
|
||||
L"</UserId><LogonType>InteractiveToken</LogonType><RunLevel>HighestAvailable</RunLevel>"
|
||||
L"</Principal></Principals>\r\n"
|
||||
L" <Settings><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>"
|
||||
L"<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>"
|
||||
L"<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>"
|
||||
L"<AllowHardTerminate>true</AllowHardTerminate><StartWhenAvailable>true</StartWhenAvailable>"
|
||||
L"<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>"
|
||||
L"<IdleSettings><StopOnIdleEnd>false</StopOnIdleEnd><RestartOnIdle>false</RestartOnIdle>"
|
||||
L"</IdleSettings><AllowStartOnDemand>true</AllowStartOnDemand><Enabled>true</Enabled>"
|
||||
L"<Hidden>false</Hidden><RunOnlyIfIdle>false</RunOnlyIfIdle>"
|
||||
L"<WakeToRun>false</WakeToRun><ExecutionTimeLimit>PT0S</ExecutionTimeLimit>"
|
||||
L"<Priority>7</Priority></Settings>\r\n"
|
||||
L" <Actions Context=\"Author\"><Exec><Command>" + xmlEscape(exe) +
|
||||
L"</Command><Arguments>--startup --elevated</Arguments><WorkingDirectory>" +
|
||||
xmlEscape(parentDirectory(exe)) +
|
||||
L"</WorkingDirectory></Exec></Actions>\r\n"
|
||||
L"</Task>\r\n";
|
||||
|
||||
bool ok = writeUtf16File(tempFile, xml, error);
|
||||
if (ok) {
|
||||
const std::wstring command = quote(schtasksPath()) + L" /Create /F /TN " +
|
||||
quote(kTaskName) + L" /XML " + quote(tempFile);
|
||||
const DWORD result = runHiddenProcess(command);
|
||||
ok = result == ERROR_SUCCESS;
|
||||
if (!ok && error) {
|
||||
*error = L"Планировщик заданий вернул код " + std::to_wstring(result) + L".";
|
||||
}
|
||||
}
|
||||
DeleteFileW(tempFile);
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool isProcessElevated() {
|
||||
HANDLE token = nullptr;
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) return false;
|
||||
TOKEN_ELEVATION elevation{};
|
||||
DWORD bytes = 0;
|
||||
const BOOL ok = GetTokenInformation(token, TokenElevation, &elevation,
|
||||
sizeof(elevation), &bytes);
|
||||
CloseHandle(token);
|
||||
return ok && elevation.TokenIsElevated != 0;
|
||||
}
|
||||
|
||||
bool ensureElevated(const wchar_t* mainWindowClass) {
|
||||
if ((mainWindowClass && FindWindowW(mainWindowClass, nullptr)) ||
|
||||
FindWindowW(kLegacyMainWindowClass, nullptr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isProcessElevated()) return true;
|
||||
|
||||
const std::vector<std::wstring> args = commandLineArguments();
|
||||
if (std::find(args.begin(), args.end(), L"--elevated") != args.end()) {
|
||||
MessageBoxW(nullptr, L"NightShot не получил права администратора.",
|
||||
L"NightShot", MB_OK | MB_ICONERROR);
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::wstring exe = executablePath();
|
||||
const std::wstring parameters = elevationArguments();
|
||||
if (exe.empty()) return false;
|
||||
|
||||
SHELLEXECUTEINFOW execution{};
|
||||
execution.cbSize = sizeof(execution);
|
||||
execution.fMask = SEE_MASK_FLAG_NO_UI;
|
||||
execution.lpVerb = L"runas";
|
||||
execution.lpFile = exe.c_str();
|
||||
execution.lpParameters = parameters.c_str();
|
||||
execution.nShow = SW_SHOWNORMAL;
|
||||
if (!ShellExecuteExW(&execution)) {
|
||||
const DWORD code = GetLastError();
|
||||
if (code != ERROR_CANCELLED) {
|
||||
const std::wstring message = L"Не удалось запустить NightShot от администратора:\n" +
|
||||
win32Error(code);
|
||||
MessageBoxW(nullptr, message.c_str(), L"NightShot", MB_OK | MB_ICONERROR);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool applyAutostart(bool enabled, bool silentAdmin, std::wstring* error) {
|
||||
if (error) error->clear();
|
||||
|
||||
// Remove stale autostart entries left by versions published as SnapLight.
|
||||
// Failure here is non-fatal because the new entry/task is still authoritative.
|
||||
deleteRunValue(kLegacyRunValue);
|
||||
deleteTask(kLegacyTaskName);
|
||||
|
||||
if (!enabled) {
|
||||
const bool runRemoved = setRunValue(false, error);
|
||||
const bool taskRemoved = deleteTask(kTaskName);
|
||||
if (!taskRemoved && error && error->empty()) {
|
||||
*error = L"Не удалось удалить задачу Silent Admin.";
|
||||
}
|
||||
return runRemoved && taskRemoved;
|
||||
}
|
||||
|
||||
if (silentAdmin) {
|
||||
if (!setRunValue(false, error)) return false;
|
||||
deleteTask(kTaskName);
|
||||
return createSilentTask(error);
|
||||
}
|
||||
|
||||
deleteTask(kTaskName);
|
||||
return setRunValue(true, error);
|
||||
}
|
||||
|
||||
} // namespace startup
|
||||
@@ -0,0 +1,350 @@
|
||||
#include "uploader.hpp"
|
||||
|
||||
#include "app_messages.hpp"
|
||||
|
||||
#include <winhttp.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cwctype>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace uploader {
|
||||
namespace {
|
||||
|
||||
struct InternetHandle {
|
||||
HINTERNET value = nullptr;
|
||||
InternetHandle() = default;
|
||||
explicit InternetHandle(HINTERNET handle) : value(handle) {}
|
||||
~InternetHandle() { if (value) WinHttpCloseHandle(value); }
|
||||
InternetHandle(const InternetHandle&) = delete;
|
||||
InternetHandle& operator=(const InternetHandle&) = delete;
|
||||
};
|
||||
|
||||
struct Job {
|
||||
HWND target = nullptr;
|
||||
std::vector<BYTE> png;
|
||||
settings::AppSettings configuration;
|
||||
};
|
||||
|
||||
std::string wideToUtf8(const std::wstring& value) {
|
||||
if (value.empty()) return {};
|
||||
const int bytes = WideCharToMultiByte(CP_UTF8, 0, value.c_str(),
|
||||
static_cast<int>(value.size()),
|
||||
nullptr, 0, nullptr, nullptr);
|
||||
if (bytes <= 0) return {};
|
||||
std::string result(static_cast<std::size_t>(bytes), '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, value.c_str(), static_cast<int>(value.size()),
|
||||
result.data(), bytes, nullptr, nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::wstring utf8ToWide(const std::string& value) {
|
||||
if (value.empty()) return {};
|
||||
int chars = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
|
||||
value.data(), static_cast<int>(value.size()),
|
||||
nullptr, 0);
|
||||
UINT codePage = CP_UTF8;
|
||||
DWORD flags = MB_ERR_INVALID_CHARS;
|
||||
if (chars <= 0) {
|
||||
codePage = CP_ACP;
|
||||
flags = 0;
|
||||
chars = MultiByteToWideChar(codePage, flags, value.data(),
|
||||
static_cast<int>(value.size()), nullptr, 0);
|
||||
}
|
||||
if (chars <= 0) return {};
|
||||
std::wstring result(static_cast<std::size_t>(chars), L'\0');
|
||||
MultiByteToWideChar(codePage, flags, value.data(), static_cast<int>(value.size()),
|
||||
result.data(), chars);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::wstring trim(std::wstring value) {
|
||||
while (!value.empty() && std::iswspace(value.front())) value.erase(value.begin());
|
||||
while (!value.empty() && std::iswspace(value.back())) value.pop_back();
|
||||
return value;
|
||||
}
|
||||
|
||||
std::wstring win32Error(DWORD code) {
|
||||
wchar_t* raw = nullptr;
|
||||
const DWORD length = FormatMessageW(
|
||||
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
|
||||
FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr, code, 0, reinterpret_cast<wchar_t*>(&raw), 0, nullptr);
|
||||
std::wstring result = length && raw
|
||||
? std::wstring(raw, length)
|
||||
: L"Ошибка Windows " + std::to_wstring(code);
|
||||
if (raw) LocalFree(raw);
|
||||
return trim(std::move(result));
|
||||
}
|
||||
|
||||
std::wstring percentEncode(const std::wstring& value) {
|
||||
static constexpr wchar_t kHex[] = L"0123456789ABCDEF";
|
||||
const std::string utf8 = wideToUtf8(value);
|
||||
std::wstring result;
|
||||
result.reserve(utf8.size() * 3);
|
||||
for (unsigned char byte : utf8) {
|
||||
if ((byte >= 'a' && byte <= 'z') ||
|
||||
(byte >= 'A' && byte <= 'Z') ||
|
||||
(byte >= '0' && byte <= '9') ||
|
||||
byte == '-' || byte == '_' || byte == '.' || byte == '~') {
|
||||
result.push_back(static_cast<wchar_t>(byte));
|
||||
} else {
|
||||
result.push_back(L'%');
|
||||
result.push_back(kHex[(byte >> 4) & 0x0F]);
|
||||
result.push_back(kHex[byte & 0x0F]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string safeDispositionValue(const std::wstring& value) {
|
||||
std::string result = wideToUtf8(value);
|
||||
for (char& ch : result) {
|
||||
if (ch == '\"' || ch == '\r' || ch == '\n' || ch == '\\') ch = '_';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void appendBytes(std::vector<BYTE>& output, const std::string& value) {
|
||||
output.insert(output.end(), value.begin(), value.end());
|
||||
}
|
||||
|
||||
std::wstring extractJsonString(const std::wstring& response,
|
||||
const std::wstring& key) {
|
||||
const std::wstring marker = L"\"" + key + L"\"";
|
||||
std::size_t position = response.find(marker);
|
||||
if (position == std::wstring::npos) return {};
|
||||
position = response.find(L':', position + marker.size());
|
||||
if (position == std::wstring::npos) return {};
|
||||
position = response.find(L'\"', position + 1);
|
||||
if (position == std::wstring::npos) return {};
|
||||
++position;
|
||||
|
||||
std::wstring result;
|
||||
bool escaped = false;
|
||||
for (; position < response.size(); ++position) {
|
||||
const wchar_t ch = response[position];
|
||||
if (escaped) {
|
||||
switch (ch) {
|
||||
case L'n': result.push_back(L'\n'); break;
|
||||
case L'r': result.push_back(L'\r'); break;
|
||||
case L't': result.push_back(L'\t'); break;
|
||||
case L'/': result.push_back(L'/'); break;
|
||||
case L'\\': result.push_back(L'\\'); break;
|
||||
case L'\"': result.push_back(L'\"'); break;
|
||||
default: result.push_back(ch); break;
|
||||
}
|
||||
escaped = false;
|
||||
} else if (ch == L'\\') {
|
||||
escaped = true;
|
||||
} else if (ch == L'\"') {
|
||||
return result;
|
||||
} else {
|
||||
result.push_back(ch);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring responseUrl(const std::wstring& raw) {
|
||||
const std::wstring response = trim(raw);
|
||||
if (response.rfind(L"https://", 0) == 0 || response.rfind(L"http://", 0) == 0) {
|
||||
const std::size_t end = response.find_first_of(L"\r\n\t ");
|
||||
return response.substr(0, end);
|
||||
}
|
||||
|
||||
for (const wchar_t* key : {L"url", L"link", L"location"}) {
|
||||
std::wstring value = trim(extractJsonString(response, key));
|
||||
if (value.rfind(L"https://", 0) == 0 || value.rfind(L"http://", 0) == 0) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::unique_ptr<Result> performUpload(const Job& job) {
|
||||
auto result = std::make_unique<Result>();
|
||||
if (job.png.empty()) {
|
||||
result->message = L"PNG для загрузки пуст.";
|
||||
return result;
|
||||
}
|
||||
|
||||
std::wstring endpoint = settings::normalizeEndpoint(job.configuration.uploadUrl);
|
||||
if (endpoint.empty()) {
|
||||
result->message = L"URL API-сервера не настроен.";
|
||||
return result;
|
||||
}
|
||||
|
||||
const wchar_t separator = endpoint.find(L'?') == std::wstring::npos ? L'?' :
|
||||
((endpoint.back() == L'?' || endpoint.back() == L'&') ? L'\0' : L'&');
|
||||
if (separator != L'\0') endpoint.push_back(separator);
|
||||
endpoint += L"name=" + percentEncode(job.configuration.uploadName);
|
||||
|
||||
URL_COMPONENTSW parts{};
|
||||
parts.dwStructSize = sizeof(parts);
|
||||
parts.dwSchemeLength = static_cast<DWORD>(-1);
|
||||
parts.dwHostNameLength = static_cast<DWORD>(-1);
|
||||
parts.dwUrlPathLength = static_cast<DWORD>(-1);
|
||||
parts.dwExtraInfoLength = static_cast<DWORD>(-1);
|
||||
if (!WinHttpCrackUrl(endpoint.c_str(), static_cast<DWORD>(endpoint.size()),
|
||||
0, &parts) || parts.dwHostNameLength == 0) {
|
||||
result->message = L"Не удалось разобрать URL API-сервера.";
|
||||
return result;
|
||||
}
|
||||
|
||||
const std::wstring host(parts.lpszHostName, parts.dwHostNameLength);
|
||||
std::wstring path;
|
||||
if (parts.dwUrlPathLength > 0) {
|
||||
path.assign(parts.lpszUrlPath, parts.dwUrlPathLength);
|
||||
}
|
||||
if (path.empty()) path = L"/";
|
||||
if (parts.dwExtraInfoLength > 0) {
|
||||
path.append(parts.lpszExtraInfo, parts.dwExtraInfoLength);
|
||||
}
|
||||
|
||||
InternetHandle session(WinHttpOpen(
|
||||
L"NightShot/0.17", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
|
||||
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0));
|
||||
if (!session.value) {
|
||||
result->message = L"WinHttpOpen: " + win32Error(GetLastError());
|
||||
return result;
|
||||
}
|
||||
WinHttpSetTimeouts(session.value, 10000, 10000, 30000, 30000);
|
||||
|
||||
InternetHandle connection(WinHttpConnect(session.value, host.c_str(),
|
||||
parts.nPort, 0));
|
||||
if (!connection.value) {
|
||||
result->message = L"WinHttpConnect: " + win32Error(GetLastError());
|
||||
return result;
|
||||
}
|
||||
|
||||
const DWORD requestFlags = parts.nScheme == INTERNET_SCHEME_HTTPS
|
||||
? WINHTTP_FLAG_SECURE : 0;
|
||||
InternetHandle request(WinHttpOpenRequest(
|
||||
connection.value, L"POST", path.c_str(), nullptr,
|
||||
WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, requestFlags));
|
||||
if (!request.value) {
|
||||
result->message = L"WinHttpOpenRequest: " + win32Error(GetLastError());
|
||||
return result;
|
||||
}
|
||||
|
||||
const std::string boundary = "----NightShotBoundary" +
|
||||
std::to_string(GetCurrentProcessId()) + std::to_string(GetTickCount64());
|
||||
const std::string field = safeDispositionValue(job.configuration.multipartField);
|
||||
const std::string filename = safeDispositionValue(job.configuration.uploadName) + ".png";
|
||||
|
||||
std::vector<BYTE> body;
|
||||
body.reserve(job.png.size() + 512);
|
||||
appendBytes(body, "--" + boundary + "\r\n");
|
||||
appendBytes(body, "Content-Disposition: form-data; name=\"" + field +
|
||||
"\"; filename=\"" + filename + "\"\r\n");
|
||||
appendBytes(body, "Content-Type: image/png\r\n\r\n");
|
||||
body.insert(body.end(), job.png.begin(), job.png.end());
|
||||
appendBytes(body, "\r\n--" + boundary + "--\r\n");
|
||||
|
||||
if (body.size() > std::numeric_limits<DWORD>::max()) {
|
||||
result->message = L"Снимок слишком большой для WinHTTP.";
|
||||
return result;
|
||||
}
|
||||
|
||||
const std::wstring headers = L"Content-Type: multipart/form-data; boundary=" +
|
||||
utf8ToWide(boundary) + L"\r\n";
|
||||
if (!WinHttpSendRequest(
|
||||
request.value, headers.c_str(), static_cast<DWORD>(headers.size()),
|
||||
body.data(), static_cast<DWORD>(body.size()),
|
||||
static_cast<DWORD>(body.size()), 0)) {
|
||||
result->message = L"WinHttpSendRequest: " + win32Error(GetLastError());
|
||||
return result;
|
||||
}
|
||||
if (!WinHttpReceiveResponse(request.value, nullptr)) {
|
||||
result->message = L"WinHttpReceiveResponse: " + win32Error(GetLastError());
|
||||
return result;
|
||||
}
|
||||
|
||||
DWORD status = 0;
|
||||
DWORD statusBytes = sizeof(status);
|
||||
WinHttpQueryHeaders(request.value,
|
||||
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
|
||||
WINHTTP_HEADER_NAME_BY_INDEX, &status, &statusBytes,
|
||||
WINHTTP_NO_HEADER_INDEX);
|
||||
result->httpStatus = status;
|
||||
|
||||
std::string responseBytes;
|
||||
constexpr std::size_t kResponseLimit = 4u * 1024u * 1024u;
|
||||
for (;;) {
|
||||
DWORD available = 0;
|
||||
if (!WinHttpQueryDataAvailable(request.value, &available)) {
|
||||
result->message = L"WinHttpQueryDataAvailable: " + win32Error(GetLastError());
|
||||
return result;
|
||||
}
|
||||
if (available == 0) break;
|
||||
if (responseBytes.size() + available > kResponseLimit) {
|
||||
result->message = L"Ответ API-сервера слишком большой.";
|
||||
return result;
|
||||
}
|
||||
const std::size_t oldSize = responseBytes.size();
|
||||
responseBytes.resize(oldSize + available);
|
||||
DWORD read = 0;
|
||||
if (!WinHttpReadData(request.value, responseBytes.data() + oldSize,
|
||||
available, &read)) {
|
||||
result->message = L"WinHttpReadData: " + win32Error(GetLastError());
|
||||
return result;
|
||||
}
|
||||
responseBytes.resize(oldSize + read);
|
||||
if (read == 0) break;
|
||||
}
|
||||
|
||||
const std::wstring response = trim(utf8ToWide(responseBytes));
|
||||
if (status < 200 || status >= 300) {
|
||||
result->message = L"API-сервер вернул HTTP " + std::to_wstring(status);
|
||||
if (!response.empty()) result->message += L":\n" + response.substr(0, 1000);
|
||||
return result;
|
||||
}
|
||||
|
||||
result->success = true;
|
||||
result->responseUrl = responseUrl(response);
|
||||
result->message = result->responseUrl.empty()
|
||||
? (response.empty() ? L"Снимок загружен." : response.substr(0, 1000))
|
||||
: L"Снимок загружен.";
|
||||
return result;
|
||||
}
|
||||
|
||||
DWORD WINAPI workerProc(void* raw) {
|
||||
std::unique_ptr<Job> job(static_cast<Job*>(raw));
|
||||
std::unique_ptr<Result> result = performUpload(*job);
|
||||
Result* transferred = result.release();
|
||||
if (!job->target || !IsWindow(job->target) ||
|
||||
!PostMessageW(job->target, WM_NIGHTSHOT_UPLOAD_FINISHED,
|
||||
0, reinterpret_cast<LPARAM>(transferred))) {
|
||||
delete transferred;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool start(HWND notificationTarget, std::vector<BYTE>&& png,
|
||||
const settings::AppSettings& configuration) {
|
||||
if (!notificationTarget || !IsWindow(notificationTarget) || png.empty()) return false;
|
||||
|
||||
auto* job = new Job();
|
||||
job->target = notificationTarget;
|
||||
job->png = std::move(png);
|
||||
job->configuration = configuration;
|
||||
|
||||
HANDLE thread = CreateThread(nullptr, 0, workerProc, job, 0, nullptr);
|
||||
if (!thread) {
|
||||
delete job;
|
||||
return false;
|
||||
}
|
||||
CloseHandle(thread);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace uploader
|
||||
Reference in New Issue
Block a user