mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-04-10 04:19:25 -07:00
314 lines
9.8 KiB
TypeScript
314 lines
9.8 KiB
TypeScript
import koffi from 'koffi';
|
|
|
|
const user32 = koffi.load('user32.dll');
|
|
const dwmapi = koffi.load('dwmapi.dll');
|
|
const kernel32 = koffi.load('kernel32.dll');
|
|
|
|
const RECT = koffi.struct('RECT', {
|
|
Left: 'int',
|
|
Top: 'int',
|
|
Right: 'int',
|
|
Bottom: 'int',
|
|
});
|
|
|
|
const MARGINS = koffi.struct('MARGINS', {
|
|
cxLeftWidth: 'int',
|
|
cxRightWidth: 'int',
|
|
cyTopHeight: 'int',
|
|
cyBottomHeight: 'int',
|
|
});
|
|
|
|
const WNDENUMPROC = koffi.proto('bool __stdcall WNDENUMPROC(intptr hwnd, intptr lParam)');
|
|
|
|
const EnumWindows = user32.func('bool __stdcall EnumWindows(WNDENUMPROC *cb, intptr lParam)');
|
|
const IsWindowVisible = user32.func('bool __stdcall IsWindowVisible(intptr hwnd)');
|
|
const IsIconic = user32.func('bool __stdcall IsIconic(intptr hwnd)');
|
|
const GetForegroundWindow = user32.func('intptr __stdcall GetForegroundWindow()');
|
|
const SetWindowPos = user32.func(
|
|
'bool __stdcall SetWindowPos(intptr hwnd, intptr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags)',
|
|
);
|
|
const GetWindowThreadProcessId = user32.func(
|
|
'uint __stdcall GetWindowThreadProcessId(intptr hwnd, _Out_ uint *lpdwProcessId)',
|
|
);
|
|
const GetWindowLongW = user32.func('int __stdcall GetWindowLongW(intptr hwnd, int nIndex)');
|
|
const SetWindowLongPtrW = user32.func(
|
|
'intptr __stdcall SetWindowLongPtrW(intptr hwnd, int nIndex, intptr dwNewLong)',
|
|
);
|
|
const GetWindowFn = user32.func('intptr __stdcall GetWindow(intptr hwnd, uint uCmd)');
|
|
const GetWindowRect = user32.func('bool __stdcall GetWindowRect(intptr hwnd, _Out_ RECT *lpRect)');
|
|
|
|
const DwmGetWindowAttribute = dwmapi.func(
|
|
'int __stdcall DwmGetWindowAttribute(intptr hwnd, uint dwAttribute, _Out_ RECT *pvAttribute, uint cbAttribute)',
|
|
);
|
|
const DwmExtendFrameIntoClientArea = dwmapi.func(
|
|
'int __stdcall DwmExtendFrameIntoClientArea(intptr hwnd, MARGINS *pMarInset)',
|
|
);
|
|
|
|
const OpenProcess = kernel32.func(
|
|
'intptr __stdcall OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId)',
|
|
);
|
|
const CloseHandle = kernel32.func('bool __stdcall CloseHandle(intptr hObject)');
|
|
const GetLastError = kernel32.func('uint __stdcall GetLastError()');
|
|
const SetLastError = kernel32.func('void __stdcall SetLastError(uint dwErrCode)');
|
|
const QueryFullProcessImageNameW = kernel32.func(
|
|
'bool __stdcall QueryFullProcessImageNameW(intptr hProcess, uint dwFlags, _Out_ uint16 *lpExeName, _Inout_ uint *lpdwSize)',
|
|
);
|
|
|
|
const GWL_EXSTYLE = -20;
|
|
const WS_EX_TOPMOST = 0x00000008;
|
|
const GWLP_HWNDPARENT = -8;
|
|
const GW_HWNDPREV = 3;
|
|
const DWMWA_EXTENDED_FRAME_BOUNDS = 9;
|
|
const PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
|
|
const SWP_NOSIZE = 0x0001;
|
|
const SWP_NOMOVE = 0x0002;
|
|
const SWP_NOACTIVATE = 0x0010;
|
|
const SWP_NOOWNERZORDER = 0x0200;
|
|
const SWP_FLAGS = SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_NOOWNERZORDER;
|
|
const HWND_TOP = 0;
|
|
const HWND_BOTTOM = 1;
|
|
const HWND_TOPMOST = -1;
|
|
const HWND_NOTOPMOST = -2;
|
|
|
|
function extendOverlayFrameIntoClientArea(overlayHwnd: number): void {
|
|
const hr = DwmExtendFrameIntoClientArea(overlayHwnd, {
|
|
cxLeftWidth: -1,
|
|
cxRightWidth: -1,
|
|
cyTopHeight: -1,
|
|
cyBottomHeight: -1,
|
|
});
|
|
if (hr !== 0) {
|
|
throw new Error(`DwmExtendFrameIntoClientArea failed (${hr})`);
|
|
}
|
|
}
|
|
|
|
function resetLastError(): void {
|
|
SetLastError(0);
|
|
}
|
|
|
|
function assertSetWindowLongPtrSucceeded(operation: string, result: number): void {
|
|
if (result !== 0) {
|
|
return;
|
|
}
|
|
|
|
if (GetLastError() === 0) {
|
|
return;
|
|
}
|
|
|
|
throw new Error(`${operation} failed (${GetLastError()})`);
|
|
}
|
|
|
|
function assertSetWindowPosSucceeded(operation: string, result: boolean): void {
|
|
if (result) {
|
|
return;
|
|
}
|
|
|
|
throw new Error(`${operation} failed (${GetLastError()})`);
|
|
}
|
|
|
|
function assertGetWindowLongSucceeded(operation: string, result: number): number {
|
|
if (result !== 0 || GetLastError() === 0) {
|
|
return result;
|
|
}
|
|
|
|
throw new Error(`${operation} failed (${GetLastError()})`);
|
|
}
|
|
|
|
export interface WindowBounds {
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
export interface MpvWindowMatch {
|
|
hwnd: number;
|
|
bounds: WindowBounds;
|
|
area: number;
|
|
isForeground: boolean;
|
|
}
|
|
|
|
export interface MpvPollResult {
|
|
matches: MpvWindowMatch[];
|
|
focusState: boolean;
|
|
windowState: 'visible' | 'minimized' | 'not-found';
|
|
}
|
|
|
|
function getWindowBounds(hwnd: number): WindowBounds | null {
|
|
const rect = { Left: 0, Top: 0, Right: 0, Bottom: 0 };
|
|
const hr = DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, rect, koffi.sizeof(RECT));
|
|
if (hr !== 0) {
|
|
if (!GetWindowRect(hwnd, rect)) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const width = rect.Right - rect.Left;
|
|
const height = rect.Bottom - rect.Top;
|
|
if (width <= 0 || height <= 0) return null;
|
|
|
|
return { x: rect.Left, y: rect.Top, width, height };
|
|
}
|
|
|
|
function getProcessNameByPid(pid: number): string | null {
|
|
const hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid);
|
|
if (!hProcess) return null;
|
|
|
|
try {
|
|
const buffer = new Uint16Array(260);
|
|
const size = new Uint32Array([260]);
|
|
|
|
if (!QueryFullProcessImageNameW(hProcess, 0, buffer, size)) {
|
|
return null;
|
|
}
|
|
|
|
const fullPath = String.fromCharCode(...buffer.slice(0, size[0]));
|
|
const fileName = fullPath.split('\\').pop() || '';
|
|
return fileName.replace(/\.exe$/i, '');
|
|
} finally {
|
|
CloseHandle(hProcess);
|
|
}
|
|
}
|
|
|
|
export function findMpvWindows(): MpvPollResult {
|
|
const foregroundHwnd = GetForegroundWindow();
|
|
const matches: MpvWindowMatch[] = [];
|
|
let hasMinimized = false;
|
|
let hasFocused = false;
|
|
const processNameCache = new Map<number, string | null>();
|
|
|
|
const cb = koffi.register((hwnd: number, _lParam: number) => {
|
|
if (!IsWindowVisible(hwnd)) return true;
|
|
|
|
const pid = new Uint32Array(1);
|
|
GetWindowThreadProcessId(hwnd, pid);
|
|
const pidValue = pid[0]!;
|
|
if (pidValue === 0) return true;
|
|
|
|
let processName = processNameCache.get(pidValue);
|
|
if (processName === undefined) {
|
|
processName = getProcessNameByPid(pidValue);
|
|
processNameCache.set(pidValue, processName);
|
|
}
|
|
|
|
if (!processName || processName.toLowerCase() !== 'mpv') return true;
|
|
|
|
if (IsIconic(hwnd)) {
|
|
hasMinimized = true;
|
|
return true;
|
|
}
|
|
|
|
const bounds = getWindowBounds(hwnd);
|
|
if (!bounds) return true;
|
|
|
|
const isForeground = foregroundHwnd !== 0 && hwnd === foregroundHwnd;
|
|
if (isForeground) hasFocused = true;
|
|
|
|
matches.push({
|
|
hwnd,
|
|
bounds,
|
|
area: bounds.width * bounds.height,
|
|
isForeground,
|
|
});
|
|
|
|
return true;
|
|
}, koffi.pointer(WNDENUMPROC));
|
|
|
|
try {
|
|
EnumWindows(cb, 0);
|
|
} finally {
|
|
koffi.unregister(cb);
|
|
}
|
|
|
|
return {
|
|
matches,
|
|
focusState: hasFocused,
|
|
windowState: matches.length > 0 ? 'visible' : hasMinimized ? 'minimized' : 'not-found',
|
|
};
|
|
}
|
|
|
|
export function getForegroundProcessName(): string | null {
|
|
const foregroundHwnd = GetForegroundWindow();
|
|
if (!foregroundHwnd) return null;
|
|
|
|
const pid = new Uint32Array(1);
|
|
GetWindowThreadProcessId(foregroundHwnd, pid);
|
|
const pidValue = pid[0]!;
|
|
if (pidValue === 0) return null;
|
|
|
|
return getProcessNameByPid(pidValue);
|
|
}
|
|
|
|
export function setOverlayOwner(overlayHwnd: number, mpvHwnd: number): void {
|
|
resetLastError();
|
|
const result = SetWindowLongPtrW(overlayHwnd, GWLP_HWNDPARENT, mpvHwnd);
|
|
assertSetWindowLongPtrSucceeded('setOverlayOwner', result);
|
|
extendOverlayFrameIntoClientArea(overlayHwnd);
|
|
}
|
|
|
|
export function ensureOverlayTransparency(overlayHwnd: number): void {
|
|
extendOverlayFrameIntoClientArea(overlayHwnd);
|
|
}
|
|
|
|
export function clearOverlayOwner(overlayHwnd: number): void {
|
|
resetLastError();
|
|
const result = SetWindowLongPtrW(overlayHwnd, GWLP_HWNDPARENT, 0);
|
|
assertSetWindowLongPtrSucceeded('clearOverlayOwner', result);
|
|
}
|
|
|
|
export function bindOverlayAboveMpv(overlayHwnd: number, mpvHwnd: number): void {
|
|
resetLastError();
|
|
const ownerResult = SetWindowLongPtrW(overlayHwnd, GWLP_HWNDPARENT, mpvHwnd);
|
|
assertSetWindowLongPtrSucceeded('bindOverlayAboveMpv owner assignment', ownerResult);
|
|
|
|
resetLastError();
|
|
const mpvExStyle = assertGetWindowLongSucceeded(
|
|
'bindOverlayAboveMpv target window style',
|
|
GetWindowLongW(mpvHwnd, GWL_EXSTYLE),
|
|
);
|
|
const mpvIsTopmost = (mpvExStyle & WS_EX_TOPMOST) !== 0;
|
|
|
|
resetLastError();
|
|
const overlayExStyle = assertGetWindowLongSucceeded(
|
|
'bindOverlayAboveMpv overlay window style',
|
|
GetWindowLongW(overlayHwnd, GWL_EXSTYLE),
|
|
);
|
|
const overlayIsTopmost = (overlayExStyle & WS_EX_TOPMOST) !== 0;
|
|
|
|
if (mpvIsTopmost && !overlayIsTopmost) {
|
|
resetLastError();
|
|
const topmostResult = SetWindowPos(overlayHwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_FLAGS);
|
|
assertSetWindowPosSucceeded('bindOverlayAboveMpv topmost adjustment', topmostResult);
|
|
} else if (!mpvIsTopmost && overlayIsTopmost) {
|
|
resetLastError();
|
|
const notTopmostResult = SetWindowPos(overlayHwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_FLAGS);
|
|
assertSetWindowPosSucceeded('bindOverlayAboveMpv notopmost adjustment', notTopmostResult);
|
|
}
|
|
|
|
const windowAboveMpv = GetWindowFn(mpvHwnd, GW_HWNDPREV);
|
|
if (windowAboveMpv !== 0 && windowAboveMpv === overlayHwnd) return;
|
|
|
|
let insertAfter = HWND_TOP;
|
|
if (windowAboveMpv !== 0) {
|
|
const aboveExStyle = GetWindowLongW(windowAboveMpv, GWL_EXSTYLE);
|
|
const aboveIsTopmost = (aboveExStyle & WS_EX_TOPMOST) !== 0;
|
|
if (aboveIsTopmost === mpvIsTopmost) {
|
|
insertAfter = windowAboveMpv;
|
|
}
|
|
}
|
|
|
|
resetLastError();
|
|
const positionResult = SetWindowPos(overlayHwnd, insertAfter, 0, 0, 0, 0, SWP_FLAGS);
|
|
assertSetWindowPosSucceeded('bindOverlayAboveMpv z-order adjustment', positionResult);
|
|
}
|
|
|
|
export function lowerOverlay(overlayHwnd: number): void {
|
|
resetLastError();
|
|
const notTopmostResult = SetWindowPos(overlayHwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_FLAGS);
|
|
assertSetWindowPosSucceeded('lowerOverlay notopmost adjustment', notTopmostResult);
|
|
|
|
resetLastError();
|
|
const bottomResult = SetWindowPos(overlayHwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_FLAGS);
|
|
assertSetWindowPosSucceeded('lowerOverlay bottom adjustment', bottomResult);
|
|
}
|