diff --git a/API_NEW/exmsw/dllmain.c b/API_NEW/exmsw/dllmain.c index 6794d93..cda2730 100644 --- a/API_NEW/exmsw/dllmain.c +++ b/API_NEW/exmsw/dllmain.c @@ -1,8 +1,5 @@ // ============================================================================ -// === EXMSW.DLL: MSWSOCK Final Implementation v2.2.0 === -// === Hybrid approach: ReactOS + Wine + Custom optimizations === -// === Author: EXLOUD (Enhanced by Claude) === -// === Purpose: Production-ready mswsock.dll emulation === +// === EXMSW.DLL: MSWSOCK Implementation === // ============================================================================ #define WIN32_LEAN_AND_MEAN @@ -18,7 +15,7 @@ #include #pragma comment(lib, "kernel32.lib") -#pragma comment(lib, "ws2_32.lib") +#pragma comment(lib, "exws2.lib") // ============================================================================ // Additional Definitions for SDK Compatibility @@ -83,10 +80,10 @@ static LPFN_ACCEPTEX g_pfnAcceptEx = NULL; static LPFN_GETACCEPTEXSOCKADDRS g_pfnGetAcceptExSockaddrs = NULL; static LPFN_TRANSMITFILE g_pfnTransmitFile = NULL; -// ws2_32.dll module handle for WSPStartup delegation -static HMODULE g_hWs2_32 = NULL; +// exws2.dll module handle for WSPStartup delegation +static HMODULE g_hexws2 = NULL; -// Function pointer types for ws2_32 functions +// Function pointer types for exws2 functions typedef int (WSAAPI *PFN_WSASTARTUP)(WORD, LPWSADATA); typedef int (WSAAPI *PFN_WSACLEANUP)(void); @@ -102,12 +99,6 @@ static FILE* g_LogFile = NULL; static CRITICAL_SECTION g_LogCS; #endif -// ============================================================================ -// Helper Macros -// ============================================================================ -#undef UNREFERENCED_PARAMETER -#define UNREFERENCED_PARAMETER(P) (void)(P) - // ============================================================================ // Error Handling // ============================================================================ @@ -154,37 +145,37 @@ static void LogMessage(const char* format, ...) { } #endif #else - UNREFERENCED_PARAMETER(format); + (void)format; #endif } // ============================================================================ -// ws2_32.dll Module Management (for WSPStartup) +// exws2.dll Module Management (for WSPStartup) // ============================================================================ -static BOOL load_ws2_32_module(void) { - if (g_hWs2_32 != NULL) { +static BOOL load_exws2_module(void) { + if (g_hexws2 != NULL) { return TRUE; } - LogMessage("Loading ws2_32.dll for WSPStartup..."); - g_hWs2_32 = LoadLibraryA("ws2_32.dll"); + LogMessage("Loading exws2.dll for WSPStartup..."); + g_hexws2 = LoadLibraryA("exws2.dll"); - if (g_hWs2_32 == NULL) { - LogMessage("Failed to load ws2_32.dll (error: %lu)", GetLastError()); + if (g_hexws2 == NULL) { + LogMessage("Failed to load exws2.dll (error: %lu)", GetLastError()); return FALSE; } - pfn_WSAStartup = (PFN_WSASTARTUP)GetProcAddress(g_hWs2_32, "WSAStartup"); - pfn_WSACleanup = (PFN_WSACLEANUP)GetProcAddress(g_hWs2_32, "WSACleanup"); + pfn_WSAStartup = (PFN_WSASTARTUP)GetProcAddress(g_hexws2, "WSAStartup"); + pfn_WSACleanup = (PFN_WSACLEANUP)GetProcAddress(g_hexws2, "WSACleanup"); - LogMessage("ws2_32.dll loaded: WSAStartup=%p, WSACleanup=%p", + LogMessage("exws2.dll loaded: WSAStartup=%p, WSACleanup=%p", pfn_WSAStartup, pfn_WSACleanup); return TRUE; } // ============================================================================ -// Wrapper Functions for ws2_32 Import +// Wrapper Functions for exws2 Import // ============================================================================ int WSAAPI ex_wrapper_getsockopt(SOCKET s, int level, int optname, char* optval, int* optlen) { return getsockopt(s, level, optname, optval, optlen); @@ -213,7 +204,7 @@ int WSAAPI ex_wrapper_setsockopt(SOCKET s, int level, int optname, * AcceptEx * * ReactOS-style implementation with caching optimization. - * Queries ws2_32 for the function pointer on first call using the + * Queries exws2 for the function pointer on first call using the * provided socket, then caches for future use. */ BOOL PASCAL FAR ex_AcceptEx( @@ -235,7 +226,7 @@ BOOL PASCAL FAR ex_AcceptEx( GUID GetAcceptExSockaddrsGUID = WSAID_GETACCEPTEXSOCKADDRS; DWORD cbBytesReturned; - LogMessage("Retrieving AcceptEx function pointer from ws2_32..."); + LogMessage("Retrieving AcceptEx function pointer from exws2..."); // Get AcceptEx if (WSAIoctl(sListenSocket, @@ -352,7 +343,7 @@ BOOL PASCAL FAR ex_TransmitFile( GUID TransmitFileGUID = WSAID_TRANSMITFILE; DWORD cbBytesReturned; - LogMessage("Retrieving TransmitFile function pointer from ws2_32..."); + LogMessage("Retrieving TransmitFile function pointer from exws2..."); if (WSAIoctl(hSocket, SIO_GET_EXTENSION_FUNCTION_POINTER, @@ -385,21 +376,14 @@ BOOL PASCAL FAR ex_TransmitFile( * * Deprecated function - not implemented. */ -int PASCAL FAR ex_WSARecvEx(SOCKET s, char* buf, int len, int* flags) +int WINAPI ex_WSARecvEx(SOCKET s, char* buf, int len, int* flags) { - LogMessage("WSARecvEx -> WSAEOPNOTSUPP (deprecated)"); - - UNREFERENCED_PARAMETER(s); - UNREFERENCED_PARAMETER(buf); - UNREFERENCED_PARAMETER(len); - UNREFERENCED_PARAMETER(flags); - - SetMSWSockError(WSAEOPNOTSUPP); + LogMessage("WSARecvEx -> SOCKET_ERROR (stub)"); return SOCKET_ERROR; } // ============================================================================ -// Protocol Enumeration Functions - Delegate to ws2_32 +// Protocol Enumeration Functions - Delegate to exws2 // ============================================================================ INT WSAAPI ex_EnumProtocolsA(LPINT lpiProtocols, LPVOID lpProtocolBuffer, LPDWORD lpdwBufferLength) { LogMessage("EnumProtocolsA -> Delegating to WSAEnumProtocolsA"); @@ -419,13 +403,7 @@ INT WSAAPI ex_GetAddressByNameA(DWORD dwNameSpace, LPGUID lpServiceType, LPSTR l LPSERVICE_ASYNC_INFO lpServiceAsyncInfo, LPVOID lpCsaddrBuffer, LPDWORD lpdwBufferLength, LPSTR lpAliasBuffer, LPDWORD lpdwAliasBufferLength) { - LogMessage("GetAddressByNameA -> WSAHOST_NOT_FOUND (deprecated, use getaddrinfo)"); - UNREFERENCED_PARAMETER(dwNameSpace); UNREFERENCED_PARAMETER(lpServiceType); - UNREFERENCED_PARAMETER(lpServiceName); UNREFERENCED_PARAMETER(lpiProtocols); - UNREFERENCED_PARAMETER(dwResolution); UNREFERENCED_PARAMETER(lpServiceAsyncInfo); - UNREFERENCED_PARAMETER(lpCsaddrBuffer); UNREFERENCED_PARAMETER(lpdwBufferLength); - UNREFERENCED_PARAMETER(lpAliasBuffer); UNREFERENCED_PARAMETER(lpdwAliasBufferLength); - SetMSWSockError(WSAHOST_NOT_FOUND); + LogMessage("GetAddressByNameA -> SOCKET_ERROR (stub)"); return SOCKET_ERROR; } @@ -434,67 +412,41 @@ INT WSAAPI ex_GetAddressByNameW(DWORD dwNameSpace, LPGUID lpServiceType, LPWSTR LPSERVICE_ASYNC_INFO lpServiceAsyncInfo, LPVOID lpCsaddrBuffer, LPDWORD lpdwBufferLength, LPWSTR lpAliasBuffer, LPDWORD lpdwAliasBufferLength) { - LogMessage("GetAddressByNameW -> WSAHOST_NOT_FOUND (deprecated)"); - UNREFERENCED_PARAMETER(dwNameSpace); UNREFERENCED_PARAMETER(lpServiceType); - UNREFERENCED_PARAMETER(lpServiceName); UNREFERENCED_PARAMETER(lpiProtocols); - UNREFERENCED_PARAMETER(dwResolution); UNREFERENCED_PARAMETER(lpServiceAsyncInfo); - UNREFERENCED_PARAMETER(lpCsaddrBuffer); UNREFERENCED_PARAMETER(lpdwBufferLength); - UNREFERENCED_PARAMETER(lpAliasBuffer); UNREFERENCED_PARAMETER(lpdwAliasBufferLength); - SetMSWSockError(WSAHOST_NOT_FOUND); + LogMessage("GetAddressByNameW -> SOCKET_ERROR (stub)"); return SOCKET_ERROR; } INT WSAAPI ex_GetNameByTypeA(LPGUID lpServiceType, LPSTR lpServiceName, DWORD dwNameLength) { - LogMessage("GetNameByTypeA -> WSATYPE_NOT_FOUND (deprecated)"); - UNREFERENCED_PARAMETER(lpServiceType); - if (lpServiceName && dwNameLength > 0) lpServiceName[0] = '\0'; - SetMSWSockError(WSATYPE_NOT_FOUND); - return SOCKET_ERROR; + LogMessage("GetNameByTypeA -> TRUE (stub)"); + return TRUE; } INT WSAAPI ex_GetNameByTypeW(LPGUID lpServiceType, LPWSTR lpServiceName, DWORD dwNameLength) { - LogMessage("GetNameByTypeW -> WSATYPE_NOT_FOUND (deprecated)"); - UNREFERENCED_PARAMETER(lpServiceType); - if (lpServiceName && dwNameLength > 0) lpServiceName[0] = L'\0'; - SetMSWSockError(WSATYPE_NOT_FOUND); - return SOCKET_ERROR; + LogMessage("GetNameByTypeW -> TRUE (stub)"); + return TRUE; } INT WSAAPI ex_GetTypeByNameA(LPSTR lpServiceName, LPGUID lpServiceType) { - LogMessage("GetTypeByNameA -> WSATYPE_NOT_FOUND (deprecated)"); - UNREFERENCED_PARAMETER(lpServiceName); UNREFERENCED_PARAMETER(lpServiceType); - SetMSWSockError(WSATYPE_NOT_FOUND); + LogMessage("GetTypeByNameA -> SOCKET_ERROR (stub)"); return SOCKET_ERROR; } INT WSAAPI ex_GetTypeByNameW(LPWSTR lpServiceName, LPGUID lpServiceType) { - LogMessage("GetTypeByNameW -> WSATYPE_NOT_FOUND (deprecated)"); - UNREFERENCED_PARAMETER(lpServiceName); UNREFERENCED_PARAMETER(lpServiceType); - SetMSWSockError(WSATYPE_NOT_FOUND); + LogMessage("GetTypeByNameW -> SOCKET_ERROR (stub)"); return SOCKET_ERROR; } INT WSAAPI ex_GetServiceA(DWORD dwNameSpace, LPGUID lpGuid, LPSTR lpServiceName, DWORD dwProperties, LPVOID lpBuffer, LPDWORD lpdwBufferSize, LPSERVICE_ASYNC_INFO lpServiceAsyncInfo) { - LogMessage("GetServiceA -> WSASERVICE_NOT_FOUND (deprecated)"); - UNREFERENCED_PARAMETER(dwNameSpace); UNREFERENCED_PARAMETER(lpGuid); - UNREFERENCED_PARAMETER(lpServiceName); UNREFERENCED_PARAMETER(dwProperties); - UNREFERENCED_PARAMETER(lpBuffer); UNREFERENCED_PARAMETER(lpdwBufferSize); - UNREFERENCED_PARAMETER(lpServiceAsyncInfo); - SetMSWSockError(WSASERVICE_NOT_FOUND); + LogMessage("GetServiceA -> SOCKET_ERROR (stub)"); return SOCKET_ERROR; } INT WSAAPI ex_GetServiceW(DWORD dwNameSpace, LPGUID lpGuid, LPWSTR lpServiceName, DWORD dwProperties, LPVOID lpBuffer, LPDWORD lpdwBufferSize, LPSERVICE_ASYNC_INFO lpServiceAsyncInfo) { - LogMessage("GetServiceW -> WSASERVICE_NOT_FOUND (deprecated)"); - UNREFERENCED_PARAMETER(dwNameSpace); UNREFERENCED_PARAMETER(lpGuid); - UNREFERENCED_PARAMETER(lpServiceName); UNREFERENCED_PARAMETER(dwProperties); - UNREFERENCED_PARAMETER(lpBuffer); UNREFERENCED_PARAMETER(lpdwBufferSize); - UNREFERENCED_PARAMETER(lpServiceAsyncInfo); - SetMSWSockError(WSASERVICE_NOT_FOUND); + LogMessage("GetServiceW -> SOCKET_ERROR (stub)"); return SOCKET_ERROR; } @@ -502,22 +454,16 @@ INT WSAAPI ex_SetServiceA(DWORD dwNameSpace, DWORD dwOperation, DWORD dwFlags, LPSERVICE_INFOA lpServiceInfo, LPSERVICE_ASYNC_INFO lpServiceAsyncInfo, LPDWORD lpdwStatusFlags) { - LogMessage("SetServiceA -> NO_ERROR (deprecated stub)"); - UNREFERENCED_PARAMETER(dwNameSpace); UNREFERENCED_PARAMETER(dwOperation); - UNREFERENCED_PARAMETER(dwFlags); UNREFERENCED_PARAMETER(lpServiceInfo); - UNREFERENCED_PARAMETER(lpServiceAsyncInfo); UNREFERENCED_PARAMETER(lpdwStatusFlags); - return NO_ERROR; + LogMessage("SetServiceA -> SOCKET_ERROR (deprecated stub)"); + return SOCKET_ERROR; } INT WSAAPI ex_SetServiceW(DWORD dwNameSpace, DWORD dwOperation, DWORD dwFlags, LPSERVICE_INFOW lpServiceInfo, LPSERVICE_ASYNC_INFO lpServiceAsyncInfo, LPDWORD lpdwStatusFlags) { - LogMessage("SetServiceW -> NO_ERROR (deprecated stub)"); - UNREFERENCED_PARAMETER(dwNameSpace); UNREFERENCED_PARAMETER(dwOperation); - UNREFERENCED_PARAMETER(dwFlags); UNREFERENCED_PARAMETER(lpServiceInfo); - UNREFERENCED_PARAMETER(lpServiceAsyncInfo); UNREFERENCED_PARAMETER(lpdwStatusFlags); - return NO_ERROR; + LogMessage("SetServiceW -> SOCKET_ERROR (deprecated stub)"); + return SOCKET_ERROR; } // ============================================================================ @@ -527,8 +473,6 @@ INT WSAAPI ex_SetServiceW(DWORD dwNameSpace, DWORD dwOperation, DWORD dwFlags, INT WSAAPI ex_Tcpip4_WSHAddressToString(LPSOCKADDR Address, INT AddressLength, LPWSAPROTOCOL_INFOW ProtocolInfo, LPWSTR AddressString, LPDWORD AddressStringLength) { - UNREFERENCED_PARAMETER(Address); UNREFERENCED_PARAMETER(AddressLength); - UNREFERENCED_PARAMETER(ProtocolInfo); if (AddressString && AddressStringLength && *AddressStringLength >= 16) { wcscpy_s(AddressString, *AddressStringLength, L"127.0.0.1"); @@ -540,15 +484,12 @@ INT WSAAPI ex_Tcpip4_WSHAddressToString(LPSOCKADDR Address, INT AddressLength, INT WSAAPI ex_Tcpip4_WSHEnumProtocols(LPINT lpiProtocols, LPWSTR lpTransportKeyName, LPVOID lpProtocolBuffer, LPDWORD lpdwBufferLength) { - UNREFERENCED_PARAMETER(lpiProtocols); UNREFERENCED_PARAMETER(lpTransportKeyName); - UNREFERENCED_PARAMETER(lpProtocolBuffer); if (lpdwBufferLength) *lpdwBufferLength = 0; return 0; } INT WSAAPI ex_Tcpip4_WSHGetBroadcastSockaddr(PVOID HelperDllSocketContext, PSOCKADDR Sockaddr, PINT SockaddrLength) { - UNREFERENCED_PARAMETER(HelperDllSocketContext); if (Sockaddr && SockaddrLength && *SockaddrLength >= sizeof(struct sockaddr_in)) { struct sockaddr_in* addr = (struct sockaddr_in*)Sockaddr; @@ -562,7 +503,6 @@ INT WSAAPI ex_Tcpip4_WSHGetBroadcastSockaddr(PVOID HelperDllSocketContext, } INT WSAAPI ex_Tcpip4_WSHGetProviderGuid(LPWSTR ProviderName, LPGUID ProviderGuid) { - UNREFERENCED_PARAMETER(ProviderName); if (ProviderGuid) { memset(ProviderGuid, 0, sizeof(GUID)); return 0; @@ -572,7 +512,6 @@ INT WSAAPI ex_Tcpip4_WSHGetProviderGuid(LPWSTR ProviderName, LPGUID ProviderGuid INT WSAAPI ex_Tcpip4_WSHGetSockaddrType(PSOCKADDR Sockaddr, DWORD SockaddrLength, PSOCKADDR_INFO SockaddrInfo) { - UNREFERENCED_PARAMETER(Sockaddr); UNREFERENCED_PARAMETER(SockaddrLength); if (SockaddrInfo) { SockaddrInfo->AddressInfo = SockaddrInfoNormal; SockaddrInfo->EndpointInfo = SockaddrEndpointRelevant; @@ -586,9 +525,6 @@ INT WSAAPI ex_Tcpip4_WSHGetSocketInformation(PVOID HelperDllSocketContext, SOCKE HANDLE TdiConnectionObjectHandle, INT Level, INT OptionName, PCHAR OptionValue, PINT OptionLength) { - UNREFERENCED_PARAMETER(HelperDllSocketContext); UNREFERENCED_PARAMETER(SocketHandle); - UNREFERENCED_PARAMETER(TdiAddressObjectHandle); UNREFERENCED_PARAMETER(TdiConnectionObjectHandle); - UNREFERENCED_PARAMETER(Level); UNREFERENCED_PARAMETER(OptionName); if (OptionValue && OptionLength && *OptionLength >= sizeof(int)) { *(int*)OptionValue = 0; @@ -601,14 +537,12 @@ INT WSAAPI ex_Tcpip4_WSHGetSocketInformation(PVOID HelperDllSocketContext, SOCKE INT WSAAPI ex_Tcpip4_WSHGetWSAProtocolInfo(LPWSTR ProviderName, LPWSAPROTOCOL_INFOW* ProtocolInfo, LPDWORD ProtocolInfoEntries) { - UNREFERENCED_PARAMETER(ProviderName); UNREFERENCED_PARAMETER(ProtocolInfo); if (ProtocolInfoEntries) *ProtocolInfoEntries = 0; return 0; } INT WSAAPI ex_Tcpip4_WSHGetWildcardSockaddr(PVOID HelperDllSocketContext, PSOCKADDR Sockaddr, PINT SockaddrLength) { - UNREFERENCED_PARAMETER(HelperDllSocketContext); if (Sockaddr && SockaddrLength && *SockaddrLength >= sizeof(struct sockaddr_in)) { struct sockaddr_in* addr = (struct sockaddr_in*)Sockaddr; @@ -622,7 +556,6 @@ INT WSAAPI ex_Tcpip4_WSHGetWildcardSockaddr(PVOID HelperDllSocketContext, } INT WSAAPI ex_Tcpip4_WSHGetWinsockMapping(PWINSOCK_MAPPING Mapping, DWORD MappingLength) { - UNREFERENCED_PARAMETER(Mapping); UNREFERENCED_PARAMETER(MappingLength); return 0; } @@ -633,12 +566,6 @@ INT WSAAPI ex_Tcpip4_WSHIoctl(PVOID HelperDllSocketContext, SOCKET SocketHandle, LPDWORD NumberOfBytesReturned, LPWSAOVERLAPPED Overlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE CompletionRoutine, LPBOOL NeedsCompletion) { - UNREFERENCED_PARAMETER(HelperDllSocketContext); UNREFERENCED_PARAMETER(SocketHandle); - UNREFERENCED_PARAMETER(TdiAddressObjectHandle); UNREFERENCED_PARAMETER(TdiConnectionObjectHandle); - UNREFERENCED_PARAMETER(IoControlCode); UNREFERENCED_PARAMETER(InputBuffer); - UNREFERENCED_PARAMETER(InputBufferLength); UNREFERENCED_PARAMETER(OutputBuffer); - UNREFERENCED_PARAMETER(OutputBufferLength); UNREFERENCED_PARAMETER(NumberOfBytesReturned); - UNREFERENCED_PARAMETER(Overlapped); UNREFERENCED_PARAMETER(CompletionRoutine); if (NeedsCompletion) *NeedsCompletion = FALSE; return 0; @@ -650,41 +577,24 @@ INT WSAAPI ex_Tcpip4_WSHJoinLeaf(PVOID HelperDllSocketContext, SOCKET SocketHand PSOCKADDR Sockaddr, DWORD SockaddrLength, LPWSABUF CallerData, LPWSABUF CalleeData, LPQOS SocketQOS, LPQOS GroupQOS, DWORD Flags) { - UNREFERENCED_PARAMETER(HelperDllSocketContext); UNREFERENCED_PARAMETER(SocketHandle); - UNREFERENCED_PARAMETER(TdiAddressObjectHandle); UNREFERENCED_PARAMETER(TdiConnectionObjectHandle); - UNREFERENCED_PARAMETER(LeafHelperDllSocketContext); UNREFERENCED_PARAMETER(LeafSocketHandle); - UNREFERENCED_PARAMETER(Sockaddr); UNREFERENCED_PARAMETER(SockaddrLength); - UNREFERENCED_PARAMETER(CallerData); UNREFERENCED_PARAMETER(CalleeData); - UNREFERENCED_PARAMETER(SocketQOS); UNREFERENCED_PARAMETER(GroupQOS); - UNREFERENCED_PARAMETER(Flags); return 0; } INT WSAAPI ex_Tcpip4_WSHNotify(PVOID HelperDllSocketContext, SOCKET SocketHandle, HANDLE TdiAddressObjectHandle, HANDLE TdiConnectionObjectHandle, DWORD NotifyEvent) { - UNREFERENCED_PARAMETER(HelperDllSocketContext); UNREFERENCED_PARAMETER(SocketHandle); - UNREFERENCED_PARAMETER(TdiAddressObjectHandle); UNREFERENCED_PARAMETER(TdiConnectionObjectHandle); - UNREFERENCED_PARAMETER(NotifyEvent); return 0; } INT WSAAPI ex_Tcpip4_WSHOpenSocket(PINT AddressFamily, PINT SocketType, PINT Protocol, PUNICODE_STRING TransportDeviceName, PVOID* HelperDllSocketContext, PDWORD NotificationEvents) { - UNREFERENCED_PARAMETER(AddressFamily); UNREFERENCED_PARAMETER(SocketType); - UNREFERENCED_PARAMETER(Protocol); UNREFERENCED_PARAMETER(TransportDeviceName); - UNREFERENCED_PARAMETER(HelperDllSocketContext); UNREFERENCED_PARAMETER(NotificationEvents); return 0; } INT WSAAPI ex_Tcpip4_WSHOpenSocket2(PINT AddressFamily, PINT SocketType, PINT Protocol, GROUP Group, DWORD Flags, PUNICODE_STRING TransportDeviceName, PVOID* HelperDllSocketContext, PDWORD NotificationEvents) { - UNREFERENCED_PARAMETER(AddressFamily); UNREFERENCED_PARAMETER(SocketType); - UNREFERENCED_PARAMETER(Protocol); UNREFERENCED_PARAMETER(Group); - UNREFERENCED_PARAMETER(Flags); UNREFERENCED_PARAMETER(TransportDeviceName); - UNREFERENCED_PARAMETER(HelperDllSocketContext); UNREFERENCED_PARAMETER(NotificationEvents); return 0; } @@ -693,18 +603,12 @@ INT WSAAPI ex_Tcpip4_WSHSetSocketInformation(PVOID HelperDllSocketContext, SOCKE HANDLE TdiConnectionObjectHandle, INT Level, INT OptionName, PCHAR OptionValue, INT OptionLength) { - UNREFERENCED_PARAMETER(HelperDllSocketContext); UNREFERENCED_PARAMETER(SocketHandle); - UNREFERENCED_PARAMETER(TdiAddressObjectHandle); UNREFERENCED_PARAMETER(TdiConnectionObjectHandle); - UNREFERENCED_PARAMETER(Level); UNREFERENCED_PARAMETER(OptionName); - UNREFERENCED_PARAMETER(OptionValue); UNREFERENCED_PARAMETER(OptionLength); return 0; } INT WSAAPI ex_Tcpip4_WSHStringToAddress(LPWSTR AddressString, DWORD AddressFamily, LPWSAPROTOCOL_INFOW ProtocolInfo, LPSOCKADDR Address, LPDWORD AddressLength) { - UNREFERENCED_PARAMETER(AddressString); UNREFERENCED_PARAMETER(AddressFamily); - UNREFERENCED_PARAMETER(ProtocolInfo); if (Address && AddressLength && *AddressLength >= sizeof(struct sockaddr_in)) { struct sockaddr_in* addr = (struct sockaddr_in*)Address; @@ -722,7 +626,6 @@ INT WSAAPI ex_Tcpip4_WSHStringToAddress(LPWSTR AddressString, DWORD AddressFamil // ============================================================================ INT WSAAPI ex_Tcpip6_WSHAddressToString(LPSOCKADDR A, INT AL, LPWSAPROTOCOL_INFOW PI, LPWSTR AS, LPDWORD ASL) { - UNREFERENCED_PARAMETER(A); UNREFERENCED_PARAMETER(AL); UNREFERENCED_PARAMETER(PI); if (AS && ASL && *ASL >= 4) { wcscpy_s(AS, *ASL, L"::1"); *ASL = 4; @@ -805,12 +708,9 @@ INT WSAAPI ex_WSPStartup(WORD wVersionRequested, LPWSPDATA lpWSPData, WSPUPCALLTABLE UpcallTable, LPWSPPROC_TABLE lpProcTable) { LogMessage("WSPStartup(version=%04X)", wVersionRequested); - UNREFERENCED_PARAMETER(lpProtocolInfo); - UNREFERENCED_PARAMETER(UpcallTable); - UNREFERENCED_PARAMETER(lpProcTable); - if (!load_ws2_32_module()) { - LogMessage("WSPStartup: Failed to load ws2_32.dll"); + if (!load_exws2_module()) { + LogMessage("WSPStartup: Failed to load exws2.dll"); return WSASYSNOTREADY; } @@ -872,16 +772,13 @@ INT WSAAPI ex_GetSocketErrorMessageW(INT ErrorCode, LPWSTR Buffer, INT BufferSiz int WSAAPI ex_NPLoadNameSpaces(LPDWORD lpdwVersion, LPNS_ROUTINE lpnsrBuffer, LPDWORD lpdwBufferLength) { - LogMessage("NPLoadNameSpaces"); - UNREFERENCED_PARAMETER(lpnsrBuffer); + LogMessage("NPLoadNameSpaces -> TRUE (stub)"); if (lpdwVersion) *lpdwVersion = 1; - if (lpdwBufferLength) *lpdwBufferLength = 0; - return 0; + return TRUE; } INT WSAAPI ex_NSPStartup(LPGUID lpProviderId, LPNSP_ROUTINE lpnspRoutines) { LogMessage("NSPStartup"); - UNREFERENCED_PARAMETER(lpProviderId); UNREFERENCED_PARAMETER(lpnspRoutines); return NO_ERROR; } @@ -889,44 +786,34 @@ void WSAAPI ex_ProcessSocketNotifications(void) { LogMessage("ProcessSocketNotifications"); } -DWORD WSAAPI ex_StartWsdpService(void) { - LogMessage("StartWsdpService"); - return ERROR_SERVICE_DISABLED; +VOID WSAAPI ex_StartWsdpService(void) { + LogMessage("StartWsdpService -> (stub)"); } -BOOL WSAAPI ex_StopWsdpService(void) { - LogMessage("StopWsdpService"); - return TRUE; +VOID WSAAPI ex_StopWsdpService(void) { + LogMessage("StopWsdpService -> (stub)"); } INT WSAAPI ex_MigrateWinsockConfiguration(DWORD dwFromVersion, DWORD dwToVersion, DWORD Reserved) { - LogMessage("MigrateWinsockConfiguration"); - UNREFERENCED_PARAMETER(dwFromVersion); UNREFERENCED_PARAMETER(dwToVersion); - UNREFERENCED_PARAMETER(Reserved); - return 0; + LogMessage("MigrateWinsockConfiguration -> SOCKET_ERROR (stub)"); + return SOCKET_ERROR; } INT WSAAPI ex_MigrateWinsockConfigurationEx(DWORD dwFromVersion, DWORD dwToVersion, LPWSTR lpszFromPath, LPWSTR lpszToPath, DWORD Reserved) { - LogMessage("MigrateWinsockConfigurationEx"); - UNREFERENCED_PARAMETER(dwFromVersion); UNREFERENCED_PARAMETER(dwToVersion); - UNREFERENCED_PARAMETER(lpszFromPath); UNREFERENCED_PARAMETER(lpszToPath); - UNREFERENCED_PARAMETER(Reserved); - return 0; + LogMessage("MigrateWinsockConfigurationEx -> SOCKET_ERROR (stub)"); + return SOCKET_ERROR; } // ============================================================================ // Unix Compatibility Functions (Blocked for security - ReactOS style) // ============================================================================ -int WSAAPI ex_dn_expand(const unsigned char* msg, const unsigned char* eom, - const unsigned char* comp, char* exp, int l) { - LogMessage("dn_expand -> -1 (not implemented)"); - UNREFERENCED_PARAMETER(msg); UNREFERENCED_PARAMETER(eom); - UNREFERENCED_PARAMETER(comp); - if (exp && l > 0) exp[0] = '\0'; - return -1; +int WSAAPI ex_dn_expand(unsigned char* msg, unsigned char* eom, + unsigned char* comp, unsigned char* exp, int l) { + LogMessage("dn_expand -> SOCKET_ERROR (not implemented)"); + return SOCKET_ERROR; } struct netent* WSAAPI ex_getnetbyname(const char* name) { @@ -936,49 +823,39 @@ struct netent* WSAAPI ex_getnetbyname(const char* name) { unsigned long WSAAPI ex_inet_network(const char* cp) { LogMessage("inet_network('%s') -> INADDR_NONE", cp ? cp : "NULL"); - UNREFERENCED_PARAMETER(cp); return INADDR_NONE; } -int WSAAPI ex_rcmd(char** a, u_short r, const char* lc, const char* rm, - const char* c, int* f) { - LogMessage("rcmd() -> BLOCKED for security"); - UNREFERENCED_PARAMETER(a); UNREFERENCED_PARAMETER(r); UNREFERENCED_PARAMETER(lc); - UNREFERENCED_PARAMETER(rm); UNREFERENCED_PARAMETER(c); UNREFERENCED_PARAMETER(f); - return -1; +SOCKET WINAPI ex_rcmd(char** a, USHORT r, char* lc, char* rm, + char* c, int* f) { + LogMessage("rcmd() -> INVALID_SOCKET (stub)"); + return INVALID_SOCKET; } -int WSAAPI ex_rexec(char** a, int r, const char* u, const char* p, - const char* c, int* f) { - LogMessage("rexec() -> BLOCKED for security"); - UNREFERENCED_PARAMETER(a); UNREFERENCED_PARAMETER(r); UNREFERENCED_PARAMETER(u); - UNREFERENCED_PARAMETER(p); UNREFERENCED_PARAMETER(c); UNREFERENCED_PARAMETER(f); - return -1; +SOCKET WINAPI ex_rexec(char** a, int r, char* u, char* p, + char* c, int* f) { + LogMessage("rexec() -> INVALID_SOCKET (stub)"); + return INVALID_SOCKET; } -int WSAAPI ex_rresvport(int* port) { - LogMessage("rresvport() -> -1 (not implemented)"); - UNREFERENCED_PARAMETER(port); - return -1; +SOCKET WINAPI ex_rresvport(int* port) { + LogMessage("rresvport() -> INVALID_SOCKET (stub)"); + return INVALID_SOCKET; } void WSAAPI ex_s_perror(const char* msg) { LogMessage("s_perror('%s')", msg ? msg : "NULL"); - UNREFERENCED_PARAMETER(msg); } -int WSAAPI ex_sethostname(const char* name, int namelen) { - LogMessage("sethostname()"); - UNREFERENCED_PARAMETER(name); UNREFERENCED_PARAMETER(namelen); - return 0; +int WINAPI ex_sethostname(char* name, int namelen) { + LogMessage("sethostname() -> SOCKET_ERROR (stub)"); + return SOCKET_ERROR; } // ============================================================================ // DLL Entry Point // ============================================================================ BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { - UNREFERENCED_PARAMETER(hModule); - UNREFERENCED_PARAMETER(lpReserved); switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: @@ -1000,21 +877,21 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserv fopen_s(&g_LogFile, path, "a"); #endif - LogMessage("=== EXMSW v2.2.0 Initialized (ReactOS Hybrid) ==="); + LogMessage("=== EXMSW ==="); } break; case DLL_PROCESS_DETACH: if (InterlockedDecrement(&g_InitCount) == 0) { - LogMessage("=== EXMSW v2.2.0 Unloading ==="); + LogMessage("=== EXMSW Unloading ==="); if (g_tlsError != TLS_OUT_OF_INDEXES) { TlsFree(g_tlsError); } - if (g_hWs2_32 != NULL) { - FreeLibrary(g_hWs2_32); - g_hWs2_32 = NULL; + if (g_hexws2 != NULL) { + FreeLibrary(g_hexws2); + g_hexws2 = NULL; } #if ENABLE_FILE_LOGGING diff --git a/API_NEW/exmsw/ucrt/arm64/EXMSW.dll b/API_NEW/exmsw/ucrt/arm64/EXMSW.dll new file mode 100644 index 0000000..a05d688 Binary files /dev/null and b/API_NEW/exmsw/ucrt/arm64/EXMSW.dll differ diff --git a/API_NEW/exmsw/ucrt/x32/EXMSW.dll b/API_NEW/exmsw/ucrt/x32/EXMSW.dll new file mode 100644 index 0000000..0e21546 Binary files /dev/null and b/API_NEW/exmsw/ucrt/x32/EXMSW.dll differ diff --git a/API_NEW/exmsw/ucrt/x64/EXMSW.dll b/API_NEW/exmsw/ucrt/x64/EXMSW.dll new file mode 100644 index 0000000..f8870bc Binary files /dev/null and b/API_NEW/exmsw/ucrt/x64/EXMSW.dll differ diff --git a/API_NEW/exmsw/version.rc b/API_NEW/exmsw/version.rc index d0b7ea9..f5b6c4f 100644 --- a/API_NEW/exmsw/version.rc +++ b/API_NEW/exmsw/version.rc @@ -1,8 +1,8 @@ // version.rc #include -#define VER_FILEVERSION 1,0,5,0 -#define VER_FILEVERSION_STR "1.0.5.0\0" +#define VER_FILEVERSION 1,0,6,0 +#define VER_FILEVERSION_STR "1.0.6.0\0" VS_VERSION_INFO VERSIONINFO FILEVERSION VER_FILEVERSION diff --git a/API_PE_Replacer/CHANGELOG.md b/API_PE_Replacer/CHANGELOG.md new file mode 100644 index 0000000..dfe68dc --- /dev/null +++ b/API_PE_Replacer/CHANGELOG.md @@ -0,0 +1,272 @@ +# Changelog + +--- + +## [1.0.13] — 2026-03-07 + +### Fixed +- **Context menu corners transparent on Linux** — replaced the default `QMenu` with a + `DarkMenu(QMenu)` subclass that sets `WA_TranslucentBackground`, + `FramelessWindowHint`, and `NoDropShadowWindowHint`, so rounded corners are truly + transparent instead of showing opaque background pixels outside the border-radius. +- **Log selection lost on right-click** — `QTextEdit` lost focus on right-click, + clearing the selection before the context menu appeared. Fixed by saving + `selectionStart` / `selectionEnd` before showing the menu and restoring the cursor + with `KeepAnchor` afterwards. +- **Log selection color changed to grey when unfocused** — when the context menu stole + focus, Qt switched the `QTextEdit` to the `Inactive` palette group, rendering the + highlight grey. Fixed by copying the `Active` `Highlight` and `HighlightedText` + palette colors into the `Inactive` group via `QPalette`. +- **Message dialog text not centered** — all `QMessageBox` dialogs displayed + left-aligned text. Introduced `CenteredMessageBox(QMessageBox)` which overrides + `showEvent` to apply `AlignHCenter | AlignVCenter` to every text `QLabel`, and two + helpers `_msg()` / `_msg_question()` that replace all eight former static calls to + `QMessageBox.warning`, `QMessageBox.information`, and `QMessageBox.question`. +- **About dialog — no spacing between info card and donations section** — the + `donations_panel` had `setContentsMargins(0, 0, 0, 0)`; changed top margin to `16px` + so the "Support" section is visually separated from the title/author card. + +### Improved +- **Dark-themed log context menu** — added `QMenu` styling to `REFINED_STYLESHEET` + (`bg_elevated` background, rounded items, `bg_overlay` hover, muted separator) and + wired it via `CustomContextMenu` policy so the system menu is never shown. +- **Font selection on all platforms** — at startup `QApplication` now iterates + `Inter → Segoe UI → DejaVu Sans → Liberation Sans → Noto Sans` and sets the first + font for which `QFont.exactMatch()` returns `True`. Eliminates the + `OpenType support missing for "Adwaita Sans"` Qt warning on GNOME/Fedora and ensures + a consistent sans-serif face on Windows and macOS as well. +- **Folder search dialog replaced with resizable `QDialog`** — the subfolder-search + prompt was a `QMessageBox` whose buttons had a fixed size and could not adapt to the + window dimensions. Replaced with `FolderSearchDialog(QDialog)`: the window is + resizable (`setSizeGripEnabled`), the three buttons sit in a `QHBoxLayout` with equal + stretch factor so they always fill the full width, the primary action uses + `variant="primary"` styling, and the text label wraps and stays centred at any size. +- **About dialog now maximisable on Windows** — `setFixedSize` replaced with + `setMinimumSize` and `WindowMaximizeButtonHint` added to window flags, so double- + clicking the title bar or pressing the maximise button correctly expands the window on + Windows (Linux already handled this natively). +- **"Show every time" checkbox styled consistently across platforms** — on Linux the + checkbox in the Language Selection dialog used the system GTK appearance instead of + the application dark theme. Applied a scoped `setStyleSheet` directly on + `show_again_checkbox` with accent-coloured indicator (`bg_overlay` background, + rounded corners, `accent` fill when checked). The API-list checkboxes in the main + window are intentionally left with their default system style. + +### Code Quality +- **W0246 — useless parent delegation** — removed the trivial `__init__` from + `CenteredMessageBox` that only called `super().__init__(*args, **kwargs)` with no + additional logic. + +### Performance +- **File addition speed** — replaced `lief.PE.parse()` in `FileProcessorWorker` with a + manual raw-header read (`_read_pe_info`): reads only ~88 bytes from two offsets + (DOS header + COFF header) instead of parsing the full PE structure. On large DLLs + this can be orders of magnitude faster. Additionally, `update_stats()` is now called + once after the entire batch completes instead of after every single file, and + duplicate detection switched from `O(n²)` `any()` to an `O(n)` set lookup. + +### Added +- **ARM64 architecture detection** — `_read_pe_info` now recognises machine type + `0xAA64` (`IMAGE_FILE_MACHINE_ARM64`) and tags the file as `arm64` in the file list. + Previously only `x86` and `x64` were detected; ARM64 binaries were silently shown as + `x86`. + +--- + +## [1.0.12] — 2026-03-05 + +### Fixed +- **Stylesheet parse errors** — `RefinedDivider`, `QTextEdit`, `QScrollArea`, and + hover-state widgets emitted Qt warnings `Could not parse stylesheet`. + Root cause: closing `}}` in plain (non-`f`) string literals was passed literally to + the CSS parser instead of being collapsed to `}`. + Fixed 7 occurrences across `RefinedDivider.__init__`, `_create_log_panel`, + `RefinedFolderDialog.__init__`, and `on_file_item_hover`. + +### Security +- **B314 (Medium) — XML injection** — replaced `import xml.etree.ElementTree as ET` + with `import defusedxml.ElementTree as ET` across all XML parsing calls + (`TranslationManager.load_language`, `get_language_name_from_xml`). + The standard library parser is vulnerable to Billion Laughs and XXE attacks + (CWE-20); `defusedxml` mitigates both. +- **B110 (Low) × 3 — Silent exception suppression** — replaced bare `except: pass` + blocks with explicit error handling: + - `get_language_name_from_xml` — prints a warning with filename and exception message + - `UniversalPEPatcher.patch_all` IAT fallthrough — emits `log_iat_parse_failed` via + `log_emitter` + - `FileProcessorWorker.run` PE metadata read — prints a warning with exception message + +### Code Quality +- `too-many-lines` — added module-level disable (2 400-line single-file GUI is an + accepted constraint) +- `attribute-defined-outside-init` × 27 — pre-declared all 27 `PEPatcherGUI` UI + attributes as `None` in `__init__` before `setup_ui()` call +- `redefined-outer-name` × 8 — renamed shadowing parameters: + `lang_code` → `language_code` / `file_lang_code` / `selected_code`, + `show_dialog` → `show_again`, `translator` → `tr` +- `too-many-nested-blocks` — extracted IAT scanning logic from `patch_all` into two + helpers: `_apply_iat_replacements()` and `_scan_iat_entries()`, reducing nesting from + 9 to ≤ 5 levels +- `invalid-name` × 4 — added method-level disables for Qt-mandated camelCase event + overrides (`mousePressEvent`, `mouseMoveEvent`, `mouseReleaseEvent`, `closeEvent`) +- `too-few-public-methods` × 6 — added class-level disables for intentionally minimal + Qt signal/widget subclasses (`PatcherLogEmitter`, `FileProcessorWorker`, + `RefinedContainer`, `RefinedSplitter`, `RefinedDivider`, `AboutDialog`) +- `too-many-instance-attributes` × 3 — added class-level disables for + `SwipeableFileItem`, `RefinedFolderDialog`, `PEPatcherGUI` +- `too-many-*` (statements/locals/branches) × 7 — added method-level disables for + `patch_all`, `PatcherWorker.run`, `LanguageDialog.__init__`, + `SwipeableFileItem.__init__`, `RefinedFolderDialog.__init__`, `setup_ui`, + `_create_settings_panel`, `AboutDialog.setup_ui`, `patching_done` +- `too-many-public-methods` — added class-level disable for `PEPatcherGUI` +- `c-extension-no-member` — added inline disable for `lief.PE.MACHINE_TYPES.AMD64` +- `line-too-long` (CSS block) — wrapped `REFINED_STYLESHEET` with + `pylint: disable/enable=line-too-long`; added inline disable for Monero address + (hash is immutable) +- `wrong-import-order` — moved `glob` before third-party imports; moved `defusedxml` + into the third-party block alongside `lief` +- `missing-final-newline` — added trailing newline at EOF + +### Dependencies +- Added `defusedxml` to `requirements.txt` + +### Tooling +- Added `run.sh` — Unix equivalent of `run.bat`: creates venv, installs dependencies, + launches `main.py` + +--- + +## [1.0.11] — 2026-03-04 + +> Static-analysis refactor pass. No functional behaviour changed. + +### Code Quality + +#### Imports +- **W0611 × 9 — unused imports removed:** + `platform`, `subprocess`, `tempfile` (stdlib); + `QGridLayout`, `QListWidget`, `QSizePolicy`, `QFont`, `QPalette`, `QTextCursor` + (PySide6) — none were referenced anywhere in the codebase. + +#### Documentation +- **C0114** — added module-level docstring describing the application purpose and + technology stack. +- **C0115 × 16** — added class docstrings to all 16 classes: + `TranslationManager`, `PatcherLogEmitter`, `PermissionsManager`, + `UniversalPEPatcher`, `FileProcessorWorker`, `FolderScannerWorker`, + `PatcherWorker`, `ThreadManager`, `LanguageDialog`, `RefinedContainer`, + `SwipeableFileItem`, `RefinedFolderDialog`, `RefinedSplitter`, `RefinedDivider`, + `AboutDialog`, `PEPatcherGUI`. +- **C0116 × 57** — added docstrings to all public functions and methods. + +#### Naming +- **C0103** — renamed `PatcherLogEmitter.log_Signal` → `log_signal` (3 occurrences: + declaration, `emit()` body, and connection in `PatcherWorker.__init__`). +- **W0621** — renamed local variable `time` → `timestamp` in `PEPatcherGUI.log()` to + avoid shadowing the standard library `time` module. +- **W0612** — renamed `no_button` → `_no_button` in `add_folder()`. + +#### Protected Access +- **W0212 / E1101** — replaced `sys._MEIPASS` bare attribute access with + `getattr(sys, '_MEIPASS', os.path.abspath("."))`. + +#### Formatting +- **C0321 × 169** — expanded all semicolon-separated compound statements onto + individual lines throughout the entire file. +- **C0303 × 117** — stripped trailing spaces from every affected line. +- **W0301 × 1** — removed stray `;` after `api_checks = {}` in `_create_settings_panel`. +- **C0325 × 2** — removed redundant parens in `not (...)` and `include_subfolders = (...)`. +- **W1309 × 2** — converted two literal f-strings with no placeholders to plain strings. + +#### Logic +- **R1705** — removed redundant `else` branch after explicit `return` in `get_base_path()`. +- **W0107** — annotated the bare `except Exception: pass` in `patch_all` with an + explanatory comment. + +#### Tooling +- Added module-level `# pylint: disable` directives for C-extension false positives + (`PySide6`, `lief`) and intentional Qt GUI patterns. + +--- + +## [1.0.10] — 2025-10-29 + +_Update main.py to v1.0.10._ + +--- + +## [1.0.9] — 2025-10-28 + +### Added +- Cancel button improvements: added early cancellation detection with multiple + checkpoints during file processing. +- Smart file removal: processed files are now automatically removed from the list when + cancellation is triggered. +- Enhanced logging: cancellation logs now show total files processed, original file + count, and remaining files. +- Language files validation: added error message when language files are missing from + the `languages` folder. + +### Fixed +- Fixed file counter synchronization when cancelling batch operations. +- Corrected remaining file count calculation after cancellation. +- Resolved issue where file list wouldn't update properly after processing multiple batches. +- Fixed UI file removal to prevent duplicate entries. +- Fixed language selection dialog to show proper error message when language files are + not found. + +### Improved +- Faster cancellation response time — now stops at the earliest possible point. +- Better user feedback with detailed cancellation statistics in logs. +- Cleaner UI state management after batch operations. +- Added 500 ms delay before showing cancellation dialog to allow UI animations to complete. +- Improved error handling for missing application resources. + +### Technical +- Refactored `PatcherWorker` to track total file count and remaining files accurately. +- Optimized file removal logic in `patching_done` to prevent duplicate removals. +- Improved permission restoration handling during cancellation. +- Enhanced `LanguageDialog` with resource validation and user-friendly error messages. + +--- + +## [1.0.8] — 2025-10-21 + +### Added +- **Dynamic language support** — application now automatically detects and loads + language files (`lang_*.xml`) from a dedicated `languages` folder. Adding a new + language requires no code changes. +- Language selection dialog on first launch with a scrollable list of available languages. +- Installer for required emulators (Inno Setup), located in the `API` folder. + +### Fixed +- Restored polished "Refined Dark Theme" across the entire application, including + dialogs and all button types. +- Buttons in About and Language Selection dialogs now correctly use the standard solid + gray style. +- Re-implemented custom-styled scrollbars for all scrollable areas. +- User language preference is now correctly saved in `settings.ini` next to the + executable when compiled with PyInstaller. + +### Improved +- **Cancellable patching** — Start Patching button dynamically changes to Cancel + during operation. +- **Thread management** — implemented `ThreadManager` to handle all background tasks + cleanly, preventing UI freezes. +- Reinstated swipe-to-delete and hover-highlight on file list items. +- Sticky headers in Settings and Logs panels; Donation title in About window. +- Fixed `build.bat` and path handling to correctly bundle language files and + `config.py` for compiled `.exe`. + +--- + +## [1.0.0] — 2025-10-19 + +_Initial release._ + +- IAT and hex patching support. +- Batch file processing with real-time logging. +- Backup and overwrite options. +- Ukrainian interface. +- Supported APIs: WINHTTP, WININET, WS2_32, SENSAPI, IPHLPAPI, URLMON, NETAPI32, + WSOCK32, WINTRUST. \ No newline at end of file diff --git a/API_PE_Replacer/config.py b/API_PE_Replacer/config.py index 421a995..df153cb 100644 --- a/API_PE_Replacer/config.py +++ b/API_PE_Replacer/config.py @@ -1,33 +1,37 @@ # -*- coding: utf-8 -*- -# Файл конфігурації -# ВАЖЛИВО: Для бінарного патчингу довжина рядка для заміни -# МАЄ ЗБІГАТИСЯ з довжиною оригінального рядка. -# Використовуйте нульові байти \x00 для вирівнювання довжини. +""" +DLL replacement configuration for PE Patcher. + +IMPORTANT: For binary (hex) patching the replacement string length +MUST match the original string length exactly. +Use null bytes \\x00 to pad if needed. +""" + DLL_REPLACEMENTS = { 1: {'name': 'WINHTTP', 'replacements': { b'winhttp.dll': b'exhttp.dll\x00', - }}, - + }}, + 2: {'name': 'WININET', 'replacements': { b'wininet.dll': b'exinet.dll\x00', - }}, - + }}, + 3: {'name': 'WS2_32', 'replacements': { b'ws2_32.dll': b'exws2.dll\x00', - }}, - + }}, + 4: {'name': 'SENSAPI', 'replacements': { b'sensapi.dll': b'exsens.dll\x00', }}, - + 5: {'name': 'IPHLPAPI', 'replacements': { b'iphlpapi.dll': b'exiphl.dll\x00\x00', }}, - + 6: {'name': 'URLMON', 'replacements': { b'urlmon.dll': b'exurlm.dll', }}, - + 7: {'name': 'NETAPI32', 'replacements': { b'netapi32.dll': b'exnetapi.dll', }}, @@ -35,7 +39,7 @@ 8: {'name': 'WSOCK32', 'replacements': { b'wsock32.dll': b'exws.dll\x00\x00\x00', }}, - + 9: {'name': 'WINTRUST', 'replacements': { b'wintrust.dll': b'extrust.dll\x00', }}, diff --git a/API_PE_Replacer/main.py b/API_PE_Replacer/main.py index 707f538..2dd4ce0 100644 --- a/API_PE_Replacer/main.py +++ b/API_PE_Replacer/main.py @@ -1,1619 +1,2555 @@ -# -*- coding: utf-8 -*- - -import sys -import os -import stat -import shutil -import platform -import subprocess -import tempfile -import re -from datetime import datetime -from pathlib import Path -from typing import List, Tuple -from configparser import ConfigParser -import xml.etree.ElementTree as ET -import glob - -import pefile -from PyQt6.QtWidgets import ( - QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, - QLabel, QTextEdit, QFrame, QFileDialog, QMessageBox, QCheckBox, QDialog, - QProgressBar, QScrollArea, QListWidget, QGraphicsDropShadowEffect, - QGridLayout, QGroupBox, QSizePolicy, QSplitter, QTextBrowser -) -from PyQt6.QtCore import ( - QThread, QObject, pyqtSignal, Qt, QTranslator, QLibraryInfo, QTimer, - QPropertyAnimation, QPoint, QEasingCurve, QParallelAnimationGroup -) -from PyQt6.QtGui import QTextCursor, QFont, QColor, QPalette - -# Імпортуємо конфігурацію із зовнішнього файлу -try: - from config import DLL_REPLACEMENTS -except ImportError: - print("❌ Error: config.py file not found or is corrupted") - sys.exit(1) -except Exception as e: - print(f"❌ Error loading configuration: {e}") - sys.exit(1) - -DONATION_ADDRESSES = { - 'bitcoin': 'bc1pfnf3ukjn6sdpdujwxav8wlv0p6k5sp5fzwnz8wmdndd57z9yym7slu5dgr', - 'ethereum': '0x671c0f7d78d777da2b576ca9b6cc559f7e048d5f', - 'monero': '43myvYnEM8q2g1AULm7dp1XzLRrjZ73VaSnCmvyhSEHHGG1e3weAUFG8RWZhSasbSz9H8jZpGv8LQ8wc9aQHjvfSKW4rt4z', - 'ton': 'UQCb_q_NLHfYC4Sj0MURw57mYlK6IQXSpOkzBZIyyXnscp7m', - 'usdt_trc20': 'TFqV65zvK6NfPbtmx1pqVxSYBCjW8Vz23K', - 'usdt_erc20': '0x671c0f7d78d777da2b576ca9b6cc559f7e048d5f', - 'usdc_erc20': '0x671c0f7d78d777da2b576ca9b6cc559f7e048d5f', - 'tron': 'TTFqV65zvK6NfPbtmx1pqVxSYBCjW8Vz23K', - 'bnb': '0x671c0f7d78d777da2b576ca9b6cc559f7e048d5f', - 'github': 'https://github.com/EXLOUD' -} - -# ============================================================================= -# 0. ГЛОБАЛЬНІ КОНФІГУРАЦІЇ -# ============================================================================= - -APP_VERSION = "1.0.10" -LANG_FOLDER = "languages" - -def sanitize_filename(filename: str) -> str: - return re.sub(r'[\\/*?:"<>|]', '_', filename) - -# ### ЗМІНА: Функції для правильного визначення шляхів ### -def resource_path(relative_path): - """ Отримує абсолютний шлях до ресурсу, вбудованого в .exe """ - try: - base_path = sys._MEIPASS - except Exception: - base_path = os.path.abspath(".") - return os.path.join(base_path, relative_path) - -def get_base_path(): - """ Повертає шлях до папки з .exe або .py файлом """ - if getattr(sys, 'frozen', False): - return os.path.dirname(sys.executable) - else: - return os.path.dirname(os.path.abspath(__file__)) - -# ============================================================================= -# 1. REFINED DARK THEME -# ============================================================================= -REFINED_PALETTE = { - 'bg_primary': '#0E0E10', 'bg_secondary': '#141417', 'bg_tertiary': '#1A1A1E', 'bg_elevated': '#202024', - 'bg_overlay': '#26262B', 'accent': '#8B7FB8', 'accent_hover': '#9D91C7', 'accent_muted': 'rgba(139, 127, 184, 0.15)', - 'accent_subtle': 'rgba(139, 127, 184, 0.08)', 'text': '#E8E6F0', 'text_secondary': '#A8A5B8', 'text_muted': '#6B6878', - 'text_disabled': '#48465A', 'success': '#6BCF7F', 'warning': '#E4A853', 'error': '#CF6679', 'info': '#64B5F6', - 'border': 'rgba(255, 255, 255, 0.04)', 'border_hover': 'rgba(139, 127, 184, 0.2)', 'shadow': 'rgba(0, 0, 0, 0.4)' -} - -STANDARD_BUTTON_STYLE = f""" - QPushButton {{ - font-size: 13px; font-weight: 500; letter-spacing: 0.5px; padding: 11px 20px; - border-radius: 8px; background-color: {REFINED_PALETTE['bg_tertiary']}; color: {REFINED_PALETTE['text_secondary']}; - border: none; - }} - QPushButton:hover {{ - background-color: {REFINED_PALETTE['bg_overlay']}; color: {REFINED_PALETTE['text']}; - }} - QPushButton:disabled {{ - background-color: {REFINED_PALETTE['bg_overlay']}; color: {REFINED_PALETTE['text_muted']}; - }} -""" - -REFINED_STYLESHEET = f""" - * {{ margin: 0; padding: 0; border: none; outline: none; }} - QWidget {{ color: {REFINED_PALETTE['text']}; font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, sans-serif; font-size: 13px; font-weight: 400; letter-spacing: 0.3px; }} - QMainWindow {{ background-color: {REFINED_PALETTE['bg_primary']}; }} - #RefinedCard {{ background-color: {REFINED_PALETTE['bg_secondary']}; border-radius: 12px; }} - #ElevatedCard {{ background-color: {REFINED_PALETTE['bg_elevated']}; border-radius: 16px; border: 1px solid {REFINED_PALETTE['border']}; }} - #AppHeader {{ background-color: {REFINED_PALETTE['bg_secondary']}; border-bottom: 1px solid {REFINED_PALETTE['border']}; padding: 28px 32px; }} - - {STANDARD_BUTTON_STYLE} - - QPushButton[variant="primary"] {{ background-color: {REFINED_PALETTE['accent']}; color: white; font-weight: 600; }} - QPushButton[variant="primary"]:hover {{ background-color: {REFINED_PALETTE['accent_hover']}; }} - QPushButton[variant="primary"]:disabled {{ background-color: {REFINED_PALETTE['accent_muted']}; color: rgba(255, 255, 255, 0.5); }} - QPushButton[variant="secondary"] {{ background-color: {REFINED_PALETTE['accent_subtle']}; color: {REFINED_PALETTE['accent']}; border: 1px solid {REFINED_PALETTE['accent_muted']}; }} - QPushButton[variant="secondary"]:hover {{ background-color: {REFINED_PALETTE['accent_muted']}; border-color: {REFINED_PALETTE['accent']}; }} - QPushButton[variant="ghost"] {{ background-color: transparent; color: {REFINED_PALETTE['text_muted']}; padding: 8px 12px; }} - QPushButton[variant="ghost"]:hover {{ background-color: {REFINED_PALETTE['bg_overlay']}; color: {REFINED_PALETTE['text_secondary']}; }} - - QLabel {{ background-color: transparent; }} - QLabel[class="h1"] {{ font-size: 32px; font-weight: 300; letter-spacing: -0.5px; color: {REFINED_PALETTE['text']}; }} - QLabel[class="h3"] {{ font-size: 18px; font-weight: 500; color: {REFINED_PALETTE['text']}; }} - #FileItem {{ background-color: {REFINED_PALETTE['bg_tertiary']}; border-radius: 0; padding: 14px 16px; border: 1px solid transparent; }} - #FileItem:hover {{ background-color: {REFINED_PALETTE['bg_overlay']}; border-color: {REFINED_PALETTE['border_hover']}; }} - #Divider {{ background-color: {REFINED_PALETTE['border']}; height: 1px; margin: 10px 0; }} - QGroupBox {{ background-color: transparent; border: 1px solid {REFINED_PALETTE['border']}; border-radius: 8px; padding-top: 16px; font-size: 11px; font-weight: 600; letter-spacing: 0.5px; text-transform: uppercase; }} - QGroupBox::title {{ subcontrol-origin: margin; left: 12px; padding: 0 8px; color: {REFINED_PALETTE['text_muted']}; background-color: {REFINED_PALETTE['bg_secondary']}; text-align: center; }} - QTextEdit {{ background-color: {REFINED_PALETTE['bg_tertiary']}; border: 1px solid {REFINED_PALETTE['border']}; border-radius: 8px; padding: 12px; font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; font-size: 12px; line-height: 1.6; color: {REFINED_PALETTE['text_secondary']}; }} - QProgressBar {{ background-color: {REFINED_PALETTE['bg_overlay']}; height: 5px; border-radius: 5px; border: none; text-align: center; margin: 0px; padding: 0px; }} - QProgressBar::chunk {{ background-color: {REFINED_PALETTE['accent']}; border-radius: 5px; margin: 0px; padding: 0px; }} - - QScrollArea QScrollBar:vertical {{ background-color: transparent; width: 16px; margin: 20px 4px 20px 4px; }} - QScrollBar:vertical {{ background-color: {REFINED_PALETTE['bg_overlay']}; width: 12px; border-radius: 6px; margin: 4px 2px; }} - QScrollBar::handle:vertical {{ background-color: {REFINED_PALETTE['text_muted']}; border-radius: 6px; min-height: 50px; margin: 2px; width: 12px; }} - QScrollBar::handle:vertical:hover {{ background-color: {REFINED_PALETTE['accent']}; width: 12px; }} - QScrollBar::handle:vertical:pressed {{ background-color: {REFINED_PALETTE['accent_hover']}; }} - QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical, QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {{ height: 0; background: transparent; }} - QScrollArea QScrollBar:horizontal {{ background-color: transparent; height: 16px; margin: 4px 20px 4px 20px; }} - QScrollBar:horizontal {{ background-color: {REFINED_PALETTE['bg_overlay']}; height: 12px; border-radius: 6px; margin: 2px 4px; }} - QScrollBar::handle:horizontal {{ background-color: {REFINED_PALETTE['text_muted']}; border-radius: 6px; min-width: 50px; margin: 2px; height: 12px; }} - QScrollBar::handle:horizontal:hover {{ background-color: {REFINED_PALETTE['accent']}; height: 12px; }} - QScrollBar::handle:horizontal:pressed {{ background-color: {REFINED_PALETTE['accent_hover']}; }} - QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal, QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {{ width: 0; background: transparent; }} - QAbstractScrollArea::corner {{ background-color: transparent; }} - - QDialog, QMessageBox {{ background-color: {REFINED_PALETTE['bg_secondary']}; }} - QMessageBox QLabel {{ color: {REFINED_PALETTE['text']}; }} -""" -def create_subtle_shadow(): - shadow = QGraphicsDropShadowEffect(); shadow.setBlurRadius(16); shadow.setXOffset(0); shadow.setYOffset(2); shadow.setColor(QColor(0, 0, 0, 80)); return shadow - -# ============================================================================= -# 2. МЕНЕДЖЕР ПЕРЕКЛАДІВ ТА НАЛАШТУВАНЬ -# ============================================================================= -class TranslationManager: - def __init__(self, lang_code='en'): - self.translations = {} - self.load_language(lang_code) - - def load_language(self, lang_code): - lang_path = resource_path(LANG_FOLDER) - - # ← НОВЕ: Перевіряємо чи папка існує - if not os.path.exists(lang_path): - print(f"⚠️ Warning: Languages folder not found: {lang_path}") - self.translations = {} - return - - filepath = os.path.join(lang_path, f"lang_{lang_code}.xml") - - # ← НОВЕ: Перевіряємо чи файл існує - if not os.path.exists(filepath): - print(f"⚠️ Warning: Translation file not found: {filepath}") - self.translations = {} - return - - try: - tree = ET.parse(filepath) - root = tree.getroot() - for string_tag in root.findall('string'): - key = string_tag.get('name') - value = string_tag.text - if key and value: - self.translations[key] = value - except Exception as e: - print(f"❌ Error loading translation file {filepath}: {e}") - self.translations = {} - - def get(self, key, *args): - template = self.translations.get(key, key) - try: - return template.format(*args) - except (IndexError, TypeError): - return template - -def get_settings_path(): - return os.path.join(get_base_path(), "settings.ini") - -def load_settings(): - path = get_settings_path() - config = ConfigParser() - if os.path.exists(path): - config.read(path, encoding='utf-8') - return ( - config.get("Settings", "language", fallback="en"), - config.getboolean("Settings", "show_dialog", fallback=True) - ) - return "en", True - -def save_settings(lang, show_dialog): - path = get_settings_path() - config = ConfigParser() - config.add_section("Settings") - config.set("Settings", "language", lang) - config.set("Settings", "show_dialog", str(show_dialog)) - with open(path, 'w', encoding='utf-8') as f: - config.write(f) - -def get_language_name_from_xml(filepath: str) -> str: - try: - tree = ET.parse(filepath) - root = tree.getroot() - lang_name_tag = root.find(".//string[@name='language_name']") - if lang_name_tag is not None and lang_name_tag.text: - return lang_name_tag.text - except Exception: - pass - basename = os.path.basename(filepath) - try: - return basename.split('_')[1].split('.')[0] - except IndexError: - return basename - -# ============================================================================= -# 3. БЕКЕНД ЛОГІКА (без змін) -# ============================================================================= -class PatcherLogEmitter(QObject): - log_signal = pyqtSignal(str, str, list) - def emit(self, key: str, level: str, args: list = None): self.log_signal.emit(key, level, args or []) - -class PermissionsManager: - def __init__(self, file_path: str, log_emitter: PatcherLogEmitter): - self.file_path, self.log_emitter = file_path, log_emitter - self.original_permissions, self.permissions_were_changed = None, False - def __enter__(self): - try: - self.original_permissions = os.stat(self.file_path).st_mode - if not (self.original_permissions & stat.S_IWUSR): - self.log_emitter.emit("log_readonly_file", "warning", [os.path.basename(self.file_path)]) - self.log_emitter.emit("log_changing_perms", "info") - os.chmod(self.file_path, self.original_permissions | stat.S_IWUSR) - self.log_emitter.emit("log_perms_changed", "success") - self.permissions_were_changed = True - return self - except Exception as e: - self.log_emitter.emit("log_perms_error", "error", [str(e)]); raise - def __exit__(self, exc_type, exc_val, exc_tb): - if self.permissions_were_changed and self.original_permissions is not None: - try: - self.log_emitter.emit("log_restoring_perms", "info") - os.chmod(self.file_path, self.original_permissions) - self.log_emitter.emit("log_perms_restored", "success") - except Exception as e: - self.log_emitter.emit("log_restore_perms_error", "error", [str(e)]) - -class UniversalPEPatcher: - def __init__(self, file_path: str, selected_apis: List[int], log_emitter: PatcherLogEmitter): - self.file_path, self.log_emitter, self.pe, self.data = file_path, log_emitter, None, None - self.active_replacements = {k: v for api_num in selected_apis for k, v in DLL_REPLACEMENTS[api_num]['replacements'].items()} - def load_file(self) -> bool: - try: - with open(self.file_path, 'rb') as f: self.data = bytearray(f.read()) - self.pe = pefile.PE(data=self.data); return True - except Exception as e: - self.log_emitter.emit("log_load_error", "error", [str(e)]); return False - def check_if_patchable(self) -> int: - if not self.data and not self.load_file(): return 0 - count, iat_details, hex_details = 0, [], [] - if hasattr(self.pe, 'DIRECTORY_ENTRY_IMPORT'): - for entry in self.pe.DIRECTORY_ENTRY_IMPORT: - dll_name = entry.dll.decode('utf-8', 'ignore').upper() if entry.dll else "" - for orig_dll_bytes in self.active_replacements: - if dll_name == orig_dll_bytes.decode('utf-8').upper(): - count += 1; iat_details.append(dll_name); break - for old_bytes in self.active_replacements: - hex_count = self.data.count(old_bytes) - if hex_count > 0: count += hex_count; hex_details.append(f"{old_bytes.decode('utf-8', 'ignore')} ({hex_count}x)") - if iat_details or hex_details: - self.log_emitter.emit("log_patch_details_header", "info") - if iat_details: self.log_emitter.emit("log_patch_details_iat", "info", [', '.join(iat_details)]) - if hex_details: self.log_emitter.emit("log_patch_details_hex", "info", [', '.join(hex_details)]) - return count - def patch_all(self) -> int: - count, iat_count, hex_patched_details = 0, 0, {} - if hasattr(self.pe, 'DIRECTORY_ENTRY_IMPORT'): - for entry in self.pe.DIRECTORY_ENTRY_IMPORT: - if not entry.dll: continue - dll_name = entry.dll.decode('utf-8', 'ignore') - for orig, repl in self.active_replacements.items(): - if dll_name.upper() == orig.decode('utf-8').upper(): - offset = self.pe.get_offset_from_rva(entry.struct.Name) - if offset and len(repl) <= len(entry.dll): - self.data[offset:offset + len(repl)] = repl - if len(repl) < len(entry.dll): self.data[offset + len(repl):offset + len(entry.dll)] = b'\x00' * (len(entry.dll) - len(repl)) - count += 1; iat_count += 1 - else: self.log_emitter.emit("log_iat_skipped_long", "warning", [dll_name]) - break - for old, new in self.active_replacements.items(): - if len(old) != len(new): - self.log_emitter.emit("log_hex_skipped_len", "warning", [old.decode('utf-8', 'ignore')]); continue - start_index, local_count = 0, 0 - while (index := self.data.find(old, start_index)) != -1: - self.data[index:index + len(old)] = new; start_index = index + len(old) - count += 1; local_count += 1 - if local_count > 0: hex_patched_details[old.decode('utf-8', 'ignore')] = (new.decode('utf-8', 'ignore'), local_count) - if count > 0: - for dll, (new_dll, cnt) in hex_patched_details.items(): self.log_emitter.emit("log_hex_patched", "info", [dll, new_dll, cnt]) - self.log_emitter.emit("log_total_changes", "success", [count]) - return count - def save(self, output_path: str) -> bool: - try: - pe_patched = pefile.PE(data=self.data); pe_patched.write(output_path); pe_patched.close() - self.log_emitter.emit("log_file_saved", "success", [os.path.basename(output_path)]); return True - except Exception as e: - self.log_emitter.emit("log_save_error", "error", [str(e)]); return False - def close(self): self.pe = None; self.data = None - -class FileProcessorWorker(QObject): - file_processed = pyqtSignal(dict); finished = pyqtSignal(int, int) - def __init__(self, file_paths): super().__init__(); self.file_paths = file_paths - def run(self): - added, error = 0, 0 - for path in self.file_paths: - try: - with open(path, 'rb') as f: - if f.read(2) != b'MZ': error += 1; continue - info = {'path': path, 'size': os.path.getsize(path), 'type': 'PE', 'arch': 'x86', 'status': 'ready', 'status_text_key': 'status_ready'} - try: - pe = pefile.PE(path, fast_load=True) - info['type'] = 'DLL' if pe.is_dll() else 'EXE' if pe.is_exe() else 'PE' - info['arch'] = 'x64' if pe.FILE_HEADER.Machine == 0x8664 else 'x86' - pe.close() - except Exception: pass - self.file_processed.emit(info); added += 1 - except Exception: error += 1 - self.finished.emit(added, error) - -class FolderScannerWorker(QObject): - finished = pyqtSignal(); file_found = pyqtSignal(str); scan_complete = pyqtSignal(list, list) - def __init__(self, folder_path, include_subfolders): - super().__init__(); self.folder_path = Path(folder_path) - self.include_subfolders = include_subfolders; self.is_cancelled = False - def run(self): - try: - found, skipped, exts = [], set(), {'.exe', '.dll', '.vst3', '.vst', '.sys', '.ocx', '.ax'} - pattern = '**/*' if self.include_subfolders else '*' - for p in self.folder_path.glob(pattern): - if self.is_cancelled: break - if p.is_dir() or p.suffix.lower() not in exts: continue - if not {'patched', 'backup'}.isdisjoint({part.lower() for part in p.parts}): skipped.add("Patched/Backup"); continue - try: - with p.open('rb') as f: - if f.read(2) == b'MZ': found.append(str(p)); self.file_found.emit(p.name) - except (IOError, PermissionError): continue - if not self.is_cancelled: self.scan_complete.emit(sorted(list(set(found))), sorted(list(skipped))) - except Exception as e: print(f"Error in FolderScannerWorker: {e}") - finally: self.finished.emit() - def cancel(self): self.is_cancelled = True - -class PatcherWorker(QObject): - log_message = pyqtSignal(str, str, list) - file_status_updated = pyqtSignal(str, str, str) - progress_updated = pyqtSignal(int) - finished = pyqtSignal(tuple, bool, int, int, int) # ← НОВЕ: +int для remaining_files - - def __init__(self, files, selected_apis, backup, overwrite): - super().__init__() - self.files, self.selected_apis = files, selected_apis - self.backup_var, self.overwrite_var = backup, overwrite - self.is_cancelled = False - self.total_files = len(files) - self.log_emitter = PatcherLogEmitter() - self.log_emitter.log_signal.connect(self.log_message) - - def cancel(self): - self.log_message.emit("log_cancel_request", "warning", []) - self.is_cancelled = True - - def run(self): - s, e, k, total = 0, 0, 0, len(self.files) - was_cancelled = False - cancelled_file_index = None - - try: - for i, info in enumerate(self.files): - if self.is_cancelled: - was_cancelled = True - cancelled_file_index = i - self.log_message.emit("log_patching_cancelled", "warning", []) - - for remaining_info in self.files[i:]: - self.file_status_updated.emit(remaining_info['path'], 'warning', 'status_cancelled') - break - - path, name = info['path'], sanitize_filename(os.path.basename(info['path'])) - self.log_message.emit("", "info", []) - self.log_message.emit("log_processing_file", "info", [f"[{i+1}/{total}]", os.path.basename(path)]) - original_file_data = None - - try: - with open(path, 'rb') as f: - original_file_data = f.read() - except Exception as read_err: - self.log_message.emit("log_read_error", "error", [str(read_err)]) - e += 1 - self.file_status_updated.emit(path, 'error', 'status_error') - self.progress_updated.emit(int((i + 1) / total * 100)) - continue - - patcher = None - try: - with PermissionsManager(path, self.log_emitter): - if self.is_cancelled: - was_cancelled = True - cancelled_file_index = i - self.log_message.emit("log_patching_cancelled", "warning", []) - for remaining_info in self.files[i:]: - self.file_status_updated.emit(remaining_info['path'], 'warning', 'status_cancelled') - break - - patcher = UniversalPEPatcher(path, self.selected_apis, self.log_emitter) - if not patcher.load_file() or patcher.check_if_patchable() == 0: - self.log_message.emit("log_nothing_to_patch", "warning", []) - k += 1 - self.file_status_updated.emit(path, 'warning', 'status_skipped') - self.progress_updated.emit(int((i + 1) / total * 100)) - continue - - if self.is_cancelled: - was_cancelled = True - cancelled_file_index = i - self.log_message.emit("log_patching_cancelled", "warning", []) - for remaining_info in self.files[i:]: - self.file_status_updated.emit(remaining_info['path'], 'warning', 'status_cancelled') - break - - p_count = patcher.patch_all() - - if p_count > 0: - if self.is_cancelled: - was_cancelled = True - cancelled_file_index = i - self.log_message.emit("log_patching_cancelled", "warning", []) - for remaining_info in self.files[i:]: - self.file_status_updated.emit(remaining_info['path'], 'warning', 'status_cancelled') - break - - if self.backup_var: - b_dir = os.path.join(os.path.dirname(path), 'backup') - os.makedirs(b_dir, exist_ok=True) - b_path, cnt = os.path.join(b_dir, name), 1 - base, ext = os.path.splitext(name) - while os.path.exists(b_path): - b_path = os.path.join(b_dir, f"{base}.backup{cnt}{ext}") - cnt += 1 - with open(b_path, 'wb') as bf: - bf.write(original_file_data) - self.log_message.emit("log_backup_saved", "info", [os.path.basename(b_path)]) - - if self.is_cancelled: - was_cancelled = True - cancelled_file_index = i - self.log_message.emit("log_patching_cancelled", "warning", []) - for remaining_info in self.files[i:]: - self.file_status_updated.emit(remaining_info['path'], 'warning', 'status_cancelled') - break - - p_dir = os.path.join(os.path.dirname(path), 'patched') - os.makedirs(p_dir, exist_ok=True) - i_path = os.path.join(p_dir, name) - - if not patcher.save(i_path): - e += 1 - self.file_status_updated.emit(path, 'error', 'status_error') - self.progress_updated.emit(int((i + 1) / total * 100)) - continue - - if self.overwrite_var: - try: - shutil.move(i_path, path) - self.log_message.emit("log_original_replaced", "info", []) - if not os.listdir(p_dir): - os.rmdir(p_dir) - except Exception as move_err: - self.log_message.emit("log_move_error", "error", [str(move_err)]) - e += 1 - self.file_status_updated.emit(path, 'error', 'status_error') - - s += 1 - self.file_status_updated.emit(path, 'success', 'status_done') - else: - k += 1 - self.file_status_updated.emit(path, 'warning', 'status_no_changes') - - except Exception as err: - self.log_message.emit("log_general_error", "error", [str(err)]) - e += 1 - self.file_status_updated.emit(path, 'error', 'status_error') - finally: - if patcher: - patcher.close() - - self.progress_updated.emit(int((i + 1) / total * 100)) - - finally: - # ← НОВЕ: Обраховуємо залишені файли - remaining_files = total - cancelled_file_index if cancelled_file_index is not None else 0 - # ← НОВЕ: Передаємо remaining_files у сигнал - self.finished.emit((s, k, e), was_cancelled, cancelled_file_index, self.total_files, remaining_files) - -class ThreadManager(QObject): - task_started = pyqtSignal(str) - task_finished = pyqtSignal(str) - error = pyqtSignal(str, str) - - def __init__(self, parent=None): - super().__init__(parent) - self.current_thread = None - self.current_worker = None - self.current_task_name = None - - def is_running(self) -> bool: - return self.current_thread is not None and self.current_thread.isRunning() - - def start_task(self, worker_class, task_name: str, *args, **kwargs) -> QObject: - if self.is_running(): - self.error.emit("dialog_op_in_progress", "") - return None - - self.current_task_name = task_name - self.current_thread = QThread() - self.current_worker = worker_class(*args, **kwargs) - worker = self.current_worker - - worker.moveToThread(self.current_thread) - self.current_thread.started.connect(worker.run) - worker.finished.connect(self.current_thread.quit) - self.current_thread.finished.connect(self._cleanup_after_thread_finish) - self.current_thread.finished.connect(worker.deleteLater) - self.current_thread.finished.connect(self.current_thread.deleteLater) - - self.current_thread.start() - self.task_started.emit(task_name) - return worker - - def _cleanup_after_thread_finish(self): - task_name = self.current_task_name - self.current_thread = None - self.current_worker = None - self.current_task_name = None - self.task_finished.emit(task_name) - - def stop_current_task(self): - if self.is_running() and hasattr(self.current_worker, 'cancel'): - self.current_worker.cancel() - -# ============================================================================= -# 4. ВІДЖЕТИ ТА ДІАЛОГИ -# ============================================================================= -class LanguageDialog(QDialog): - def __init__(self, parent=None): - super().__init__(parent) - self.setWindowTitle("Language Selection") - self.setMinimumWidth(400) - self.setMinimumHeight(300) - self.language = "en" # Default - - main_layout = QVBoxLayout(self) - main_layout.setContentsMargins(24, 24, 24, 24) - main_layout.setSpacing(20) - - title_label = QLabel("Select Language") - title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - title_label.setProperty("class", "h3") - main_layout.addWidget(title_label) - - # ← НОВЕ: Перевіряємо чи існує папка languages - lang_path = resource_path(LANG_FOLDER) - lang_files = glob.glob(os.path.join(lang_path, "lang_*.xml")) if os.path.exists(lang_path) else [] - - if not lang_files: - # ← НОВЕ: Папки нема або в ній немає мовних файлів - error_label = QLabel( - "⚠️ Language files not found!\n\n" - "Please ensure the 'languages' folder exists with translation files:\n" - "- languages/lang_en.xml\n\n" - "Copy the language files from the application directory and try again." - ) - error_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - error_label.setWordWrap(True) - error_label.setStyleSheet(f"color: {REFINED_PALETTE['warning']}; font-size: 12px;") - main_layout.addWidget(error_label, 1) - - # Кнопка для закриття - close_btn = QPushButton("Exit") - close_btn.setStyleSheet(STANDARD_BUTTON_STYLE) - close_btn.clicked.connect(self.reject) - btn_layout = QHBoxLayout() - btn_layout.addStretch() - btn_layout.addWidget(close_btn) - btn_layout.addStretch() - main_layout.addLayout(btn_layout) - else: - # ← НОВЕ: Мови знайдені - показуємо їх як раніше - scroll_area = QScrollArea() - scroll_area.setWidgetResizable(True) - scroll_area.setFrameShape(QFrame.Shape.NoFrame) - scroll_area.setStyleSheet("background: transparent;") - - button_container = QWidget() - buttons_layout = QVBoxLayout(button_container) - buttons_layout.setContentsMargins(0, 0, 0, 0) - buttons_layout.setSpacing(8) - - available_languages = {} - for f in lang_files: - lang_code = os.path.basename(f).split('_')[1].split('.')[0] - lang_name = get_language_name_from_xml(f) - available_languages[lang_code] = lang_name - - for code, name in sorted(available_languages.items()): - btn = QPushButton(name) - btn.setStyleSheet(STANDARD_BUTTON_STYLE) - btn.clicked.connect(lambda _, c=code: self.set_language(c)) - buttons_layout.addWidget(btn) - - buttons_layout.addStretch() - button_container.setLayout(buttons_layout) - scroll_area.setWidget(button_container) - main_layout.addWidget(scroll_area) - - self.show_again_checkbox = QCheckBox("Show every time") - self.show_again_checkbox.setChecked(True) - main_layout.addWidget(self.show_again_checkbox) - - def set_language(self, lang_code): - self.language = lang_code - self.accept() - - def get_selection(self): - if hasattr(self, 'show_again_checkbox'): - return self.language, self.show_again_checkbox.isChecked() - return self.language, False - - -class RefinedContainer(QWidget): - def __init__(self, container_type="card", parent=None): - super().__init__(parent); self.setObjectName({"card": "RefinedCard", "elevated": "ElevatedCard"}.get(container_type, "RefinedCard")) - self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) - if container_type == "elevated": self.setGraphicsEffect(create_subtle_shadow()) - -class SwipeableFileItem(QWidget): - removed = pyqtSignal(str) - def __init__(self, file_info, translator): - super().__init__() - self.file_info = file_info; self.translator = translator - self.start_pos = None; self.current_pos = 0; self.swipe_threshold = 60; self.is_swiped = False - wrapper_layout = QVBoxLayout(self); wrapper_layout.setContentsMargins(0, 0, 0, 0); wrapper_layout.setSpacing(0) - file_widget = QWidget(); file_widget.setObjectName("FileItem"); file_widget.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True); file_widget.setFixedHeight(56) - main_layout = QHBoxLayout(file_widget); main_layout.setContentsMargins(0, 0, 0, 0); main_layout.setSpacing(0) - self.content_widget = QWidget(); self.content_widget.setStyleSheet("background: transparent;") - content_layout = QHBoxLayout(self.content_widget); content_layout.setContentsMargins(16, 0, 16, 0); content_layout.setSpacing(12) - info_layout = QVBoxLayout(); info_layout.setSpacing(2); info_layout.setContentsMargins(0, 0, 0, 0) - name = QLabel(os.path.basename(file_info['path'])); name.setProperty("class", "subtitle"); name.setStyleSheet(f"color: {REFINED_PALETTE['text']};"); info_layout.addWidget(name) - details = QLabel(f"{file_info['type']} · {self._format_size(file_info['size'])} · {file_info['arch']}"); details.setProperty("class", "caption"); info_layout.addWidget(details) - content_layout.addLayout(info_layout, 1) - self.status_label = QLabel(self.translator.get(file_info.get('status_text_key', 'status_ready'))); self.status_label.setProperty("class", "caption"); content_layout.addWidget(self.status_label) - remove_btn = QPushButton("×"); remove_btn.setProperty("variant", "ghost"); remove_btn.setFixedSize(24, 24) - remove_btn.setStyleSheet("""QPushButton { font-size: 18px; padding: 0; border-radius: 4px; color: #6B6878; } QPushButton:hover { color: #CF6679; background-color: rgba(207, 102, 121, 0.1); }""") - remove_btn.setCursor(Qt.CursorShape.PointingHandCursor); remove_btn.clicked.connect(lambda: self.removed.emit(self.file_info['path'])); content_layout.addWidget(remove_btn) - self.delete_icon = QLabel("🗑️"); self.delete_icon.setProperty("class", "caption"); self.delete_icon.setStyleSheet(f"color: {REFINED_PALETTE['error']}; padding: 0 16px;"); self.delete_icon.setAlignment(Qt.AlignmentFlag.AlignCenter); self.delete_icon.hide() - main_layout.addWidget(self.content_widget); main_layout.addWidget(self.delete_icon) - self.animation = QPropertyAnimation(self.content_widget, b"pos"); self.animation.setDuration(200); self.animation.finished.connect(self.on_animation_finished) - wrapper_layout.addWidget(file_widget) - divider = QFrame(); divider.setFixedHeight(1); divider.setStyleSheet(f"background-color: {REFINED_PALETTE['border']}; margin: 0;"); wrapper_layout.addWidget(divider) - def _format_size(self, size): - for unit in ['B', 'KB', 'MB', 'GB']: - if size < 1024.0: return f"{size:.1f}{unit}" - size /= 1024.0 - return f"{size:.1f}TB" - def mousePressEvent(self, event): - if event.button() == Qt.MouseButton.LeftButton: self.start_pos = event.pos() - def mouseMoveEvent(self, event): - if self.start_pos is not None and event.buttons() & Qt.MouseButton.LeftButton: - delta = event.pos().x() - self.start_pos.x() - if delta < 0: - self.current_pos = max(delta, -self.swipe_threshold); self.content_widget.move(self.current_pos, 0) - if abs(self.current_pos) > self.swipe_threshold * 0.7: self.delete_icon.show() - else: self.delete_icon.hide() - def mouseReleaseEvent(self, event): - if self.start_pos is not None: - if abs(self.current_pos) > self.swipe_threshold * 0.8: - self.is_swiped = True; self.animation.setStartValue(self.content_widget.pos()); self.animation.setEndValue(QPoint(-self.width(), 0)); self.animation.start() - else: - self.animation.setStartValue(self.content_widget.pos()); self.animation.setEndValue(QPoint(0, 0)); self.animation.start(); self.delete_icon.hide() - self.start_pos = None - def on_animation_finished(self): - if self.is_swiped: self.removed.emit(self.file_info['path']) - def update_status(self, status: str, text: str): - self.status_label.setText(text); colors = {'success': REFINED_PALETTE['success'], 'warning': REFINED_PALETTE['warning'], 'error': REFINED_PALETTE['error'], 'ready': REFINED_PALETTE['text_muted']} - self.status_label.setStyleSheet(f"color: {colors.get(status, colors['ready'])};") - -class RefinedFolderDialog(QDialog): - files_changed = pyqtSignal() # Сигнал для оновлення UI - - def update_display(self): - """Оновлює відображення після видалення файлу""" - file_count = len(self.found_files) - - if file_count == 0: - # Немає файлів - показуємо empty state всередину контейнера - self.scroll_area.hide() - self.empty_state_internal.show() - self.status_label.setText(self.translator.get("files_not_added")) - else: - # Є файли - показуємо scroll area - self.empty_state_internal.hide() - self.scroll_area.show() - self.status_label.setText(self.translator.get("found_n_files", file_count)) - - # Оновлюємо кнопку - self.update_buttons(is_scanning=False, found_count=file_count) - - def __init__(self, parent, folder_path, include_subfolders): - super().__init__(parent) - self.found_files, self.is_closing = [], False - self.translator = parent.translator - self.setWindowTitle(self.translator.get("scan_folder_title")) - self.setFixedSize(600, 640) - self.setModal(True) - - layout = QVBoxLayout(self) - layout.setContentsMargins(24, 24, 24, 24) - layout.setSpacing(16) - - # =============== HEADER =============== - header_widget = QWidget() - header_widget.setStyleSheet(f"background-color: {REFINED_PALETTE['bg_secondary']}; border-radius: 12px; padding: 16px;") - header_layout = QVBoxLayout(header_widget) - header_layout.setContentsMargins(0, 0, 0, 0) - header_layout.setSpacing(12) - - title_label = QLabel(self.translator.get("scan_folder_title")) - title_label.setProperty("class", "h3") - title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - header_layout.addWidget(title_label) - - # Path контейнер з горизонтальним скролом - path_scroll = QScrollArea() - path_scroll.setWidgetResizable(True) - path_scroll.setFixedHeight(80) - path_scroll.setFrameShape(QFrame.Shape.NoFrame) - path_scroll.setStyleSheet(f""" - QScrollArea {{ - background-color: {REFINED_PALETTE['bg_tertiary']}; - border: none; - border-radius: 8px; - }} - QScrollBar:horizontal {{ - background-color: {REFINED_PALETTE['bg_overlay']}; - height: 8px; - border-radius: 4px; - margin: 2px; - }} - QScrollBar::handle:horizontal {{ - background-color: {REFINED_PALETTE['text_muted']}; - border-radius: 4px; - min-width: 50px; - margin: 1px; - }} - QScrollBar::handle:horizontal:hover {{ - background-color: {REFINED_PALETTE['accent']}; - }} - QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{ - width: 0; - background: transparent; - }} - """) - - path_label = QLabel(folder_path) - path_label.setProperty("class", "mono") - path_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - path_label.setStyleSheet(f"color: {REFINED_PALETTE['text_secondary']}; padding: 12px;") - path_scroll.setWidget(path_label) - header_layout.addWidget(path_scroll) - - layout.addWidget(header_widget) - - # =============== STATUS =============== - self.status_label = QLabel(self.translator.get("scanning_status_searching")) - self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - layout.addWidget(self.status_label) - - # =============== FILES CONTENT AREA =============== - self.central_container = QWidget() - self.central_layout = QVBoxLayout(self.central_container) - self.central_layout.setContentsMargins(0, 0, 0, 0) - self.central_layout.setSpacing(0) - - # Empty state - центровано у контейнері зі стилем - empty_scroll = QScrollArea() - empty_scroll.setWidgetResizable(True) - empty_scroll.setFrameShape(QFrame.Shape.NoFrame) - empty_scroll.setStyleSheet("background: transparent;") - - self.empty_state = RefinedContainer("elevated") - self.empty_state.setStyleSheet(f"background-color: {REFINED_PALETTE['bg_secondary']}; border-radius: 12px;") - empty_layout = QVBoxLayout(self.empty_state) - empty_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) - empty_layout.setSpacing(12) - empty_layout.setContentsMargins(24, 24, 24, 24) - - self.empty_text = QLabel(self.translator.get("files_not_added")) - self.empty_text.setProperty("class", "h3") - self.empty_text.setAlignment(Qt.AlignmentFlag.AlignCenter) - empty_layout.addWidget(self.empty_text) - - empty_scroll.setWidget(self.empty_state) - self.central_layout.addWidget(empty_scroll, 1) - - # Files list - self.files_container = RefinedContainer("elevated") - self.files_container.setStyleSheet(f""" - background-color: {REFINED_PALETTE['bg_tertiary']}; - border: none; - border-radius: 8px; - """) - self.files_layout = QVBoxLayout(self.files_container) - self.files_layout.setContentsMargins(12, 12, 12, 12) - self.files_layout.setSpacing(0) - - # Scroll area для файлів всередині контейнера - scroll = QScrollArea() - scroll.setWidgetResizable(True) - scroll.setFrameShape(QFrame.Shape.NoFrame) - scroll.setStyleSheet("background: transparent; border: none;") - scroll.hide() - - self.files_content = QWidget() - self.files_content.setStyleSheet("background: transparent;") - self.files_main_layout = QVBoxLayout(self.files_content) - self.files_main_layout.setContentsMargins(0, 0, 0, 0) - self.files_main_layout.setSpacing(0) - self.files_main_layout.setAlignment(Qt.AlignmentFlag.AlignTop) - - scroll.setWidget(self.files_content) - self.files_layout.addWidget(scroll) - self.scroll_area = scroll - - # Empty state для всередину контейнера (поверх scroll area) - self.empty_state_internal = QWidget() - self.empty_state_internal.setStyleSheet("background: transparent;") - empty_internal_layout = QVBoxLayout(self.empty_state_internal) - empty_internal_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) - empty_internal_layout.setSpacing(12) - - empty_internal_text = QLabel(self.translator.get("files_not_added")) - empty_internal_text.setProperty("class", "h3") - empty_internal_text.setAlignment(Qt.AlignmentFlag.AlignCenter) - empty_internal_text.setStyleSheet(f"color: {REFINED_PALETTE['text_muted']};") - empty_internal_layout.addWidget(empty_internal_text) - - self.files_layout.addWidget(self.empty_state_internal) - self.empty_state_internal.hide() - - self.central_layout.addWidget(self.files_container, 1) - layout.addWidget(self.central_container, 1) - - # =============== INFO CONTAINER =============== - self.info_container = QWidget() - self.info_container.setStyleSheet( - f"background-color: {REFINED_PALETTE['bg_tertiary']}; border-radius: 8px; padding: 12px;" - ) - info_layout = QVBoxLayout(self.info_container) - info_layout.setContentsMargins(12, 12, 12, 12) - info_layout.setSpacing(6) - - self.skipped_label = QLabel() - self.skipped_label.setProperty("class", "caption") - self.skipped_label.setStyleSheet(f"color: {REFINED_PALETTE['text_muted']};") - self.skipped_label.setWordWrap(True) - info_layout.addWidget(self.skipped_label) - self.info_container.hide() - layout.addWidget(self.info_container) - - # =============== PROGRESS BAR =============== - self.progress_bar = QProgressBar() - self.progress_bar.setRange(0, 0) - self.progress_bar.setTextVisible(False) - self.progress_bar.setFixedHeight(3) - layout.addWidget(self.progress_bar) - - # =============== BUTTONS =============== - self.btn_layout = QHBoxLayout() - self.btn_layout.setSpacing(12) - layout.addLayout(self.btn_layout) - self.update_buttons(is_scanning=True) - - # =============== THREAD SETUP =============== - self.thread = QThread(self) - self.worker = FolderScannerWorker(folder_path, include_subfolders) - self.worker.moveToThread(self.thread) - - self.thread.started.connect(self.worker.run) - self.worker.finished.connect(self.thread.quit) - self.thread.finished.connect(self.on_thread_finished) - self.worker.scan_complete.connect(self.on_scan_complete) - self.worker.file_found.connect(self.on_file_found) - - self.thread.start() - - # Підключаємо сигнал в кінці ініціалізації - self.files_changed.connect(self.update_display) - - def update_buttons(self, is_scanning=False, found_count=0): - while self.btn_layout.count(): - item = self.btn_layout.takeAt(0) - if item.widget(): - item.widget().deleteLater() - - self.btn_layout.addStretch() - if is_scanning: - cancel_btn = QPushButton(self.translator.get("cancel")) - cancel_btn.setStyleSheet(STANDARD_BUTTON_STYLE) - cancel_btn.clicked.connect(self.reject) - self.btn_layout.addWidget(cancel_btn) - else: - if found_count > 0: - add_btn = QPushButton(self.translator.get("add_n_files", found_count)) - add_btn.setStyleSheet(STANDARD_BUTTON_STYLE) - add_btn.setProperty("variant", "primary") - add_btn.clicked.connect(self.accept) - self.btn_layout.addWidget(add_btn) - close_btn = QPushButton(self.translator.get("close")) - close_btn.setStyleSheet(STANDARD_BUTTON_STYLE) - close_btn.clicked.connect(self.reject) - self.btn_layout.addWidget(close_btn) - self.btn_layout.addStretch() - - def on_file_found(self, filename): - """Викликається коли знайдено файл""" - file_widget = QWidget() - file_layout = QHBoxLayout(file_widget) - file_layout.setContentsMargins(12, 8, 12, 8) - file_layout.setSpacing(12) - - file_item = QLabel(filename) - file_item.setProperty("class", "caption") - file_item.setStyleSheet(f"color: {REFINED_PALETTE['text_secondary']};") - file_item.setWordWrap(True) - file_layout.addWidget(file_item, 1) - - # Кнопка видалення - remove_btn = QPushButton("×") - remove_btn.setProperty("variant", "ghost") - remove_btn.setFixedSize(24, 24) - remove_btn.setStyleSheet(""" - QPushButton { - font-size: 18px; - padding: 0; - border-radius: 4px; - color: #6B6878; - } - QPushButton:hover { - color: #CF6679; - background-color: rgba(207, 102, 121, 0.1); - } - """) - remove_btn.setCursor(Qt.CursorShape.PointingHandCursor) - remove_btn.clicked.connect(lambda: self.remove_file_item(file_widget)) - file_layout.addWidget(remove_btn) - - # Стилізація файлу при наведенні - file_widget.setStyleSheet(f""" - QWidget {{ - background-color: transparent; - border-radius: 6px; - padding: 0px; - }} - """) - file_widget.enterEvent = lambda e: self.on_file_item_hover(file_widget, True) - file_widget.leaveEvent = lambda e: self.on_file_item_hover(file_widget, False) - - self.files_main_layout.addWidget(file_widget) - - # Роздільник між файлами - if self.files_main_layout.count() > 1: - divider = QFrame() - divider.setFrameShape(QFrame.Shape.HLine) - divider.setFrameShadow(QFrame.Shadow.Plain) - divider.setLineWidth(1) - divider.setFixedHeight(1) - divider.setStyleSheet(f"QFrame {{ background-color: {REFINED_PALETTE['border']}; margin: 0; border: none; }}") - self.files_main_layout.insertWidget(self.files_main_layout.count() - 1, divider) - - # Показуємо список при першому знайденому файлі - if self.files_main_layout.count() <= 2: # 1 або 2 (widget + divider) - # Приховуємо empty state scroll і показуємо файли scroll - for i in range(self.central_layout.count()): - widget = self.central_layout.itemAt(i).widget() - if isinstance(widget, QScrollArea): - if widget.widget() == self.empty_state: - widget.hide() - self.scroll_area.show() - - def on_file_item_hover(self, widget, is_hovering): - """Обведення при наведенні""" - if is_hovering: - widget.setStyleSheet(f""" - QWidget {{ - background-color: {REFINED_PALETTE['bg_overlay']}; - border-radius: 6px; - padding: 0px; - }} - """) - else: - widget.setStyleSheet(f""" - QWidget {{ - background-color: transparent; - border-radius: 6px; - padding: 0px; - }} - """) - - def remove_file_item(self, widget): - """Видалення файлу з списку через кнопку""" - index = self.files_main_layout.indexOf(widget) - if index >= 0: - self.files_main_layout.removeWidget(widget) - widget.deleteLater() - - # Видаляємо роздільник якщо це не останній файл - if index < self.files_main_layout.count(): - next_widget = self.files_main_layout.itemAt(index).widget() - if isinstance(next_widget, QFrame): - self.files_main_layout.removeWidget(next_widget) - next_widget.deleteLater() - elif index > 0: - prev_widget = self.files_main_layout.itemAt(index - 1).widget() - if isinstance(prev_widget, QFrame): - self.files_main_layout.removeWidget(prev_widget) - prev_widget.deleteLater() - - # Підраховуємо кількість файлів (без роздільників) - file_count = sum(1 for i in range(self.files_main_layout.count()) - if not isinstance(self.files_main_layout.itemAt(i).widget(), QFrame)) - - # Оновлюємо found_files список - self.found_files = [] - for i in range(self.files_main_layout.count()): - widget_item = self.files_main_layout.itemAt(i).widget() - if not isinstance(widget_item, QFrame): - # Витягуємо ім'я файлу з QLabel - for j in range(widget_item.layout().count()): - child = widget_item.layout().itemAt(j).widget() - if isinstance(child, QLabel) and child != widget_item.layout().itemAt(j + 1).widget() if j + 1 < widget_item.layout().count() else None: - self.found_files.append(child.text()) - break - - # Оновлюємо UI - self.files_changed.emit() - - def on_scan_complete(self, found, skipped): - self.found_files = found - self.progress_bar.setRange(0, 100) - self.progress_bar.setValue(100) - - if skipped: - self.skipped_label.setText( - self.translator.get("skipped_folders_info") + ", ".join(skipped) - ) - self.info_container.show() - - if found: - self.status_label.setText(self.translator.get("found_n_files", len(found))) - else: - # Ніякого файла не знайдено - показуємо empty state - self.scroll_area.hide() - for i in range(self.central_layout.count()): - widget = self.central_layout.itemAt(i).widget() - if isinstance(widget, QScrollArea): - if widget.widget() == self.empty_state: - widget.show() - self.status_label.setText(self.translator.get("files_not_added")) - - self.update_buttons(is_scanning=False, found_count=len(found)) - - def on_thread_finished(self): - if self.thread: - self.thread.deleteLater() - if self.worker: - self.worker.deleteLater() - self.thread, self.worker = None, None - if self.is_closing: - super().reject() - - def get_found_files(self): - return self.found_files - - def reject(self): - if self.thread and self.thread.isRunning(): - if not self.is_closing: - self.is_closing = True - self.status_label.setText(self.translator.get("cancelling")) - for i in range(self.btn_layout.count()): - if item := self.btn_layout.itemAt(i): - if widget := item.widget(): - widget.setEnabled(False) - self.worker.cancel() - else: - super().reject() - - def closeEvent(self, event): - event.ignore() - self.reject() - -class RefinedSplitter(QSplitter): - def __init__(self, o, p=None): super().__init__(o, p); self.setHandleWidth(20); self.setStyleSheet("QSplitter { background-color: transparent; }") -class RefinedDivider(QFrame): - def __init__(self, p=None): super().__init__(p); self.setFrameShape(QFrame.Shape.HLine); self.setFrameShadow(QFrame.Shadow.Plain); self.setLineWidth(1); self.setFixedHeight(1); self.setStyleSheet(f"QFrame {{ background-color: {REFINED_PALETTE['border']}; margin: 6px 0; border: none; }}") - -class AboutDialog(QDialog): - def __init__(self, parent, translator): - super().__init__(parent) - self.translator = translator - self.setWindowTitle(self.translator.get("about")) - self.setFixedSize(600, 690) - self.setup_ui() - - def setup_ui(self): - main_layout = QVBoxLayout(self); main_layout.setContentsMargins(24, 24, 24, 24); main_layout.setSpacing(16) - about_text = f"""

{self.translator.get("app_title")} {APP_VERSION}


{self.translator.get("author")}: github.com/EXLOUD

""" - text_browser = QTextBrowser(); text_browser.setHtml(about_text); text_browser.setReadOnly(True); text_browser.setOpenExternalLinks(True) - text_browser.setFixedHeight(120) - - donations_panel = QWidget() - donations_panel_layout = QVBoxLayout(donations_panel) - donations_panel_layout.setContentsMargins(0, 0, 0, 0) - donations_panel_layout.setSpacing(8) - - title_label = QLabel(self.translator.get("donation_title")) - title_label.setProperty("class", "caption") - donations_panel_layout.addWidget(title_label) - - scroll_area = QScrollArea(); scroll_area.setWidgetResizable(True); scroll_area.setFrameShape(QFrame.Shape.NoFrame); scroll_area.setStyleSheet("background: transparent;") - - button_container = QWidget() - buttons_layout = QVBoxLayout(button_container) - buttons_layout.setContentsMargins(0, 8, 0, 0) - buttons_layout.setSpacing(8) - - address_buttons = [("Bitcoin", "bitcoin"), ("Ethereum", "ethereum"), ("Monero", "monero"), ("TON", "ton"), ("USDT (TRC20)", "usdt_trc20"), ("USDT (ERC20)", "usdt_erc20"), ("USDC (ERC20)", "usdc_erc20"), ("Tron", "tron"), ("BNB", "bnb")] - for name, key in address_buttons: - btn = QPushButton(f"📋 {name}") - btn.setStyleSheet(STANDARD_BUTTON_STYLE) - btn.setCursor(Qt.CursorShape.PointingHandCursor) - btn.clicked.connect(lambda checked, addr_key=key: (QApplication.clipboard().setText(DONATION_ADDRESSES[addr_key]), self.parent().log("log_copied", "success", [addr_key.upper(), DONATION_ADDRESSES[addr_key][:15]]))) - buttons_layout.addWidget(btn) - - buttons_layout.addStretch() - button_container.setLayout(buttons_layout) - scroll_area.setWidget(button_container) - - donations_panel_layout.addWidget(scroll_area) - - splitter = QSplitter(Qt.Orientation.Vertical) - splitter.addWidget(text_browser) - splitter.addWidget(donations_panel) - splitter.setSizes([120, 520]) - splitter.setStyleSheet("QSplitter::handle { height: 1px; background-color: transparent; }") - - main_layout.addWidget(splitter) - - close_btn = QPushButton(self.translator.get("close")); close_btn.setStyleSheet(STANDARD_BUTTON_STYLE); close_btn.setFixedWidth(120); close_btn.clicked.connect(self.accept) - btn_layout = QHBoxLayout(); btn_layout.addStretch(); btn_layout.addWidget(close_btn); btn_layout.addStretch(); main_layout.addLayout(btn_layout) - -# ============================================================================= -# 5. ГОЛОВНЕ ВІКНО -# ============================================================================= -class PEPatcherGUI(QMainWindow): - def __init__(self, translator: TranslationManager): - super().__init__() - self.translator = translator - self.files, self.file_items = [], {} - self.thread_manager = ThreadManager(self); self.thread_manager.error.connect(self.show_task_error) - self.setMinimumSize(960, 780); self.center_window(); self.setup_ui() - self.retranslate_ui() - self.log("log_app_started", "info", [self.translator.get('app_title'), APP_VERSION]) - - def center_window(self): - if screen := self.screen(): g = screen.availableGeometry(); self.move((g.width() - self.width()) // 2, (g.height() - self.height()) // 2) - - def setup_ui(self): - main = QWidget(); self.setCentralWidget(main); layout = QVBoxLayout(main); layout.setContentsMargins(0, 0, 0, 0); layout.setSpacing(0) - self._create_header(layout) - content = QWidget(); content_layout = QVBoxLayout(content); content_layout.setContentsMargins(24, 24, 24, 24); content_layout.setSpacing(0) - self.horizontal_splitter = RefinedSplitter(Qt.Orientation.Horizontal); left_panel = self._create_left_panel(); self.horizontal_splitter.addWidget(left_panel) - self.vertical_splitter = RefinedSplitter(Qt.Orientation.Vertical) - settings_panel = self._create_settings_panel() - log_panel = self._create_log_panel() - self.vertical_splitter.addWidget(settings_panel) - self.vertical_splitter.addWidget(log_panel) - self.vertical_splitter.setSizes([300, 300]); self.horizontal_splitter.addWidget(self.vertical_splitter) - self.horizontal_splitter.setSizes([600, 400]); content_layout.addWidget(self.horizontal_splitter); layout.addWidget(content, 1) - self._create_bottom(layout) - - def _create_header(self, layout): - header = QWidget(); header.setObjectName("AppHeader"); header_layout = QHBoxLayout(header) - title_section = QWidget(); title_layout = QVBoxLayout(title_section); title_layout.setContentsMargins(0,0,0,0); title_layout.setSpacing(2) - self.title_label = QLabel(); self.title_label.setProperty("class", "h1"); title_layout.addWidget(self.title_label) - self.subtitle_label = QLabel(); self.subtitle_label.setProperty("class", "caption"); title_layout.addWidget(self.subtitle_label) - header_layout.addWidget(title_section); header_layout.addStretch() - stats_section = QWidget(); stats_layout = QHBoxLayout(stats_section); stats_layout.setSpacing(24) - self.files_count = QLabel("0"); self.files_count.setProperty("class", "h1"); self.files_count.setStyleSheet(f"color: {REFINED_PALETTE['accent']};"); stats_layout.addWidget(self.files_count) - self.files_label = QLabel(); self.files_label.setProperty("class", "caption"); stats_layout.addWidget(self.files_label) - header_layout.addWidget(stats_section); layout.addWidget(header) - - def _create_left_panel(self): - container = QWidget(); layout = QVBoxLayout(container); layout.setContentsMargins(0, 0, 0, 0); layout.setSpacing(12) - file_actions_container = RefinedContainer("card"); file_actions_layout = QVBoxLayout(file_actions_container) - file_actions_layout.setContentsMargins(20, 16, 20, 16); file_actions_layout.setSpacing(12); header_layout_top = QHBoxLayout(); header_layout_top.setSpacing(12) - self.add_files_btn = QPushButton(); self.add_files_btn.clicked.connect(self.add_files); header_layout_top.addWidget(self.add_files_btn, 1) - self.add_folder_btn = QPushButton(); self.add_folder_btn.clicked.connect(self.add_folder); header_layout_top.addWidget(self.add_folder_btn, 1) - file_actions_layout.addLayout(header_layout_top); layout.addWidget(file_actions_container) - files_panel = self._create_files_panel(); layout.addWidget(files_panel, 1) - buttons_container = RefinedContainer("card"); buttons_layout = QHBoxLayout(buttons_container) - buttons_layout.setContentsMargins(20, 16, 20, 16); buttons_layout.setSpacing(12) - self.main_action_btn = QPushButton(); self.main_action_btn.clicked.connect(self.on_main_action_click); buttons_layout.addWidget(self.main_action_btn, 1) - self.clear_btn = QPushButton(); self.clear_btn.clicked.connect(self.clear_all); buttons_layout.addWidget(self.clear_btn, 1) - layout.addWidget(buttons_container); return container - - def _create_files_panel(self): - container = RefinedContainer("elevated"); layout = QVBoxLayout(container); layout.setContentsMargins(0, 0, 0, 0); layout.setSpacing(0) - scroll = QScrollArea(); scroll.setWidgetResizable(True); scroll.setFrameShape(QFrame.Shape.NoFrame); scroll.setStyleSheet("background: transparent;") - self.files_content = QWidget(); self.files_content.setStyleSheet("background: transparent;"); self.files_main_layout = QVBoxLayout(self.files_content); self.files_main_layout.setContentsMargins(0, 0, 0, 0); self.files_main_layout.setSpacing(0) - self.empty_state = QWidget(); self.empty_state.setObjectName("EmptyState"); self.empty_state.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True); self.empty_state.setMinimumHeight(250) - empty_layout = QVBoxLayout(self.empty_state); empty_layout.setAlignment(Qt.AlignmentFlag.AlignCenter); empty_layout.setSpacing(12) - self.empty_text = QLabel(); self.empty_text.setProperty("class", "h3"); self.empty_text.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.empty_hint = QLabel(); self.empty_hint.setProperty("class", "caption"); self.empty_hint.setAlignment(Qt.AlignmentFlag.AlignCenter) - empty_layout.addWidget(self.empty_text); empty_layout.addWidget(self.empty_hint) - self.files_container = QWidget(); self.files_container.setStyleSheet("background: transparent;"); self.files_layout = QVBoxLayout(self.files_container); self.files_layout.setContentsMargins(12, 12, 12, 12); self.files_layout.setSpacing(0); self.files_layout.setAlignment(Qt.AlignmentFlag.AlignTop) - self.files_main_layout.addWidget(self.empty_state); self.files_main_layout.addWidget(self.files_container); self.files_container.hide() - scroll.setWidget(self.files_content); layout.addWidget(scroll, 1); return container - - def _create_settings_panel(self): - settings = RefinedContainer("card"); settings_layout = QVBoxLayout(settings); settings_layout.setContentsMargins(0, 0, 0, 0); settings_layout.setSpacing(0) - header_widget = QWidget() - header_widget.setStyleSheet(f""" background-color: {REFINED_PALETTE['bg_secondary']}; border-top-left-radius: 12px; border-top-right-radius: 12px; padding-bottom: 10px; """) - header_layout = QVBoxLayout(header_widget); header_layout.setContentsMargins(20, 12, 20, 0); header_layout.setSpacing(0) - self.settings_title = QLabel(); self.settings_title.setProperty("class", "h3"); self.settings_title.setAlignment(Qt.AlignmentFlag.AlignCenter); header_layout.addWidget(self.settings_title) - settings_layout.addWidget(header_widget) - scroll = QScrollArea(); scroll.setWidgetResizable(True); scroll.setFrameShape(QFrame.Shape.NoFrame); scroll.setStyleSheet("background: transparent;") - settings_content = QWidget(); content_layout = QVBoxLayout(settings_content); content_layout.setContentsMargins(20, 20, 20, 20); content_layout.setSpacing(16) - self.api_group = QGroupBox(); api_layout = QVBoxLayout(self.api_group); api_layout.setContentsMargins(12, 12, 12, 12); api_layout.setSpacing(12) - self.all_apis = QCheckBox(); self.all_apis.setChecked(True); self.all_apis.stateChanged.connect(self.toggle_apis); api_layout.addWidget(self.all_apis) - api_layout.addWidget(RefinedDivider()) - self.api_checks = {}; - for num, data in DLL_REPLACEMENTS.items(): - check = QCheckBox(data['name']); check.setChecked(True); check.stateChanged.connect(self.on_api_change) - self.api_checks[num] = check; api_layout.addWidget(check) - api_layout.addStretch(); content_layout.addWidget(self.api_group) - self.options_group = QGroupBox(); options_layout = QVBoxLayout(self.options_group); options_layout.setContentsMargins(12, 12, 12, 12); options_layout.setSpacing(8) - self.backup = QCheckBox(); self.backup.setChecked(True); options_layout.addWidget(self.backup) - self.overwrite = QCheckBox(); self.overwrite.setChecked(True); options_layout.addWidget(self.overwrite) - content_layout.addWidget(self.options_group); content_layout.addStretch() - scroll.setWidget(settings_content); settings_layout.addWidget(scroll, 1); return settings - - def _create_log_panel(self): - log_panel = RefinedContainer("card"); log_layout = QVBoxLayout(log_panel); log_layout.setContentsMargins(0, 0, 0, 0); log_layout.setSpacing(0) - header_widget = QWidget(); header_widget.setStyleSheet(f" background-color: {REFINED_PALETTE['bg_secondary']}; border-top-left-radius: 12px; border-top-right-radius: 12px; padding-bottom: 10px; ") - header_layout = QHBoxLayout(header_widget); header_layout.setContentsMargins(20, 12, 20, 0); header_layout.setSpacing(12) - self.log_title = QLabel(); self.log_title.setProperty("class", "h3"); header_layout.addWidget(self.log_title); header_layout.addStretch() - self.clear_log_btn = QPushButton(); self.clear_log_btn.setProperty("variant", "ghost"); self.clear_log_btn.clicked.connect(self.clear_log); header_layout.addWidget(self.clear_log_btn) - log_layout.addWidget(header_widget) - self.log_text = QTextEdit(); self.log_text.setReadOnly(True) - self.log_text.setStyleSheet(f""" QTextEdit {{ padding: 20px; border: none; border-radius: 0; border-bottom-left-radius: 12px; border-bottom-right-radius: 12px; background-color: {REFINED_PALETTE['bg_tertiary']}; font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; font-size: 12px; }} """) - log_layout.addWidget(self.log_text, 1); return log_panel - - def _create_bottom(self, layout): - bottom = QWidget(); bottom.setStyleSheet(f"background: {REFINED_PALETTE['bg_secondary']}; border-top: 1px solid {REFINED_PALETTE['border']};") - bottom_layout = QVBoxLayout(bottom); bottom_layout.setContentsMargins(24, 16, 24, 16); bottom_layout.setSpacing(12) - self.progress = QProgressBar(); self.progress.setTextVisible(False); self.progress.setFixedHeight(5); bottom_layout.addWidget(self.progress) - btn_layout = QHBoxLayout(); btn_layout.addStretch() - self.about_btn = QPushButton(); self.about_btn.setProperty("variant", "ghost"); self.about_btn.clicked.connect(self.show_about); btn_layout.addWidget(self.about_btn) - btn_layout.addStretch(); bottom_layout.addLayout(btn_layout); layout.addWidget(bottom) - - def retranslate_ui(self): - self.setWindowTitle(f"{self.translator.get('app_title')} v{APP_VERSION}") - self.title_label.setText(self.translator.get('app_title')) - self.subtitle_label.setText(self.translator.get("version") + f" {APP_VERSION}") - self.files_label.setText(self.translator.get("files")) - self.add_files_btn.setText(self.translator.get("add_files")) - self.add_folder_btn.setText(self.translator.get("add_folder")) - self.main_action_btn.setText(self.translator.get("start_patching")) - self.clear_btn.setText(self.translator.get("clear_all")) - self.empty_text.setText(self.translator.get("files_not_added")) - self.empty_hint.setText(self.translator.get("add_pe_files_hint")) - self.settings_title.setText(self.translator.get("settings")) - self.log_title.setText(self.translator.get("logs")) - self.clear_log_btn.setText(self.translator.get("clear")) - self.about_btn.setText(self.translator.get("about")) - self.api_group.setTitle(self.translator.get("api_group_title")) - self.all_apis.setText(self.translator.get("all_apis")) - self.options_group.setTitle(self.translator.get("options_group_title")) - self.backup.setText(self.translator.get("create_backups")) - self.overwrite.setText(self.translator.get("overwrite_originals")) - - def log(self, msg_key: str, level="info", args: list = None): - if args is None: args = [] - formatted_msg = self.translator.get(msg_key, *args) - if not formatted_msg: self.log_text.append(""); return - colors = {'info': REFINED_PALETTE['text_secondary'], 'success': REFINED_PALETTE['success'], 'warning': REFINED_PALETTE['warning'], 'error': REFINED_PALETTE['error']} - time = datetime.now().strftime("%H:%M:%S") - self.log_text.append(f'{time} {formatted_msg}') - - def show_task_error(self, title_key, message): QMessageBox.warning(self, self.translator.get(title_key), message) - - def add_files(self): - files, _ = QFileDialog.getOpenFileNames(self, self.translator.get("dialog_select_pe_files"), "", self.translator.get("dialog_pe_files_filter")) - if files: self.process_files(files) - - def add_folder(self): - msg_box = QMessageBox(self); msg_box.setWindowTitle(self.translator.get("dialog_search_option")); msg_box.setText(self.translator.get("dialog_search_subfolders")) - yes_button = msg_box.addButton(self.translator.get("dialog_yes_recursive"), QMessageBox.ButtonRole.YesRole) - no_button = msg_box.addButton(self.translator.get("dialog_no_folder_only"), QMessageBox.ButtonRole.NoRole) - cancel_button = msg_box.addButton(self.translator.get("dialog_cancel"), QMessageBox.ButtonRole.RejectRole); msg_box.exec() - if msg_box.clickedButton() == cancel_button: return - include_subfolders = (msg_box.clickedButton() == yes_button) - folder = QFileDialog.getExistingDirectory(self, self.translator.get("dialog_select_folder")) - if folder: - self.log('log_scanning_folder', 'info', [folder, self.translator.get('log_with_subfolders') if include_subfolders else '']) - dialog = RefinedFolderDialog(self, folder, include_subfolders) - if dialog.exec() == QDialog.DialogCode.Accepted and (files := dialog.get_found_files()): self.process_files(files) - - def process_files(self, paths): - new = [p for p in paths if not any(f['path'] == p for f in self.files)] - if not new: self.log("log_all_files_added", "warning"); return - self.progress.setRange(0, 0) - worker = self.thread_manager.start_task(FileProcessorWorker, self.translator.get("task_analyzing_files"), new) - if not worker: self.progress.setRange(0, 100); return - worker.file_processed.connect(self.add_file_item); worker.finished.connect(self.files_added) - - def add_file_item(self, info): - if self.empty_state.isVisible(): self.empty_state.hide(); self.files_container.show() - item = SwipeableFileItem(info, self.translator); item.removed.connect(self.remove_file_with_animation) - self.files.append(info); self.file_items[info['path']] = item - self.files_layout.addWidget(item); self.update_stats() - - def files_added(self, added, errors): - self.progress.setRange(0, 100); self.progress.setValue(0) - if added: self.log("log_files_added", "success", [added]) - if errors: self.log("log_files_skipped", "warning", [errors]) - - def remove_file_with_animation(self, path): - if path in self.file_items: self.animate_card_removal(path) - - def animate_card_removal(self, path: str): - if not (widget := self.file_items.get(path)): return - anim_group = QParallelAnimationGroup(widget) - slide_anim = QPropertyAnimation(widget, b"pos"); slide_anim.setDuration(300); slide_anim.setStartValue(widget.pos()); slide_anim.setEndValue(QPoint(widget.width() * -1, widget.pos().y())); slide_anim.setEasingCurve(QEasingCurve.Type.InCubic) - shrink_anim = QPropertyAnimation(widget, b"maximumHeight"); shrink_anim.setDuration(250); shrink_anim.setStartValue(widget.height()); shrink_anim.setEndValue(0) - anim_group.addAnimation(slide_anim); anim_group.addAnimation(shrink_anim) - anim_group.finished.connect(lambda: self.finalize_removal(path, widget)); anim_group.start() - - def finalize_removal(self, path, item): - self.files = [f for f in self.files if f['path'] != path] - self.file_items.pop(path, None); item.deleteLater() - if not self.files: self.empty_state.show(); self.files_container.hide() - self.update_stats() - - def clear_all(self): - if self.thread_manager.is_running(): - QMessageBox.warning(self, self.translator.get("dialog_op_in_progress"), self.translator.get("dialog_cannot_clear")) - return - if not self.files: - QMessageBox.information(self, self.translator.get("dialog_list_empty"), self.translator.get("dialog_no_files_to_clear")); return - if QMessageBox.question(self, self.translator.get("dialog_confirmation"), self.translator.get("dialog_clear_all_q")) == QMessageBox.StandardButton.Yes: - for item in self.file_items.values(): item.deleteLater() - self.files.clear(); self.file_items.clear() - self.empty_state.show(); self.files_container.hide() - self.update_stats(); self.log("log_list_cleared", "info") - - def clear_log(self): self.log_text.clear(); self.log("log_cleared", "info") - def update_stats(self): self.files_count.setText(str(len(self.files))) - def toggle_apis(self, state): - for check in self.api_checks.values(): check.setChecked(bool(state)) - def on_api_change(self): - all_checked = all(c.isChecked() for c in self.api_checks.values()) - self.all_apis.blockSignals(True); self.all_apis.setChecked(all_checked); self.all_apis.blockSignals(False) - - def on_main_action_click(self): - if self.thread_manager.is_running(): - self.thread_manager.stop_current_task() - self.main_action_btn.setText(self.translator.get("cancelling")) - self.main_action_btn.setEnabled(False) - else: self.start_patching() - - def set_ui_for_patching(self, is_patching: bool): - """Деактивує/активує UI під час патчингу""" - if is_patching: - # Під час патчингу - деактивуємо кнопки - self.main_action_btn.setText(self.translator.get("cancel")) - self.main_action_btn.setEnabled(True) - self.clear_btn.setEnabled(False) - self.add_files_btn.setEnabled(False) - self.add_folder_btn.setEnabled(False) - for item in self.file_items.values(): - item.setEnabled(False) - else: - # Після патчингу - активуємо кнопки назад - self.main_action_btn.setText(self.translator.get("start_patching")) - self.main_action_btn.setEnabled(True) - self.clear_btn.setEnabled(True) - self.add_files_btn.setEnabled(True) - self.add_folder_btn.setEnabled(True) - for item in self.file_items.values(): - item.setEnabled(True) - - def start_patching(self): - if not self.files: - QMessageBox.warning(self, self.translator.get("warning_title"), self.translator.get("warning_no_files")) - return - - apis = [key for key, checkbox in self.api_checks.items() if checkbox.isChecked()] - if self.all_apis.isChecked(): - apis = list(DLL_REPLACEMENTS.keys()) - if not apis: - QMessageBox.warning(self, self.translator.get("warning_title"), self.translator.get("warning_no_api")) - return - - self.set_ui_for_patching(True) - self.progress.setValue(0) - - worker = self.thread_manager.start_task( - PatcherWorker, - self.translator.get("task_patching_files"), - list(self.files), - apis, - self.backup.isChecked(), - self.overwrite.isChecked() - ) - - if not worker: - self.set_ui_for_patching(False) - return - - worker.log_message.connect(self.log) - worker.progress_updated.connect(self.progress.setValue) - worker.file_status_updated.connect(self.on_file_status_updated) - worker.finished.connect(self.patching_done) - - - def on_file_status_updated(self, path: str, status: str, text_key: str): - """ - Оновлює статус файлу в UI. - - text_key може бути: - - 'status_done' → Success → НЕ анімуємо (видаляється в patching_done) - - 'status_skipped' → Skipped (оброблена) → НЕ анімуємо (видаляється в patching_done) - - 'status_cancelled' → Cancelled (скасована) → НЕ анімуємо (залишається в списку) - - 'status_no_changes' → No changes → НЕ анімуємо (видаляється в patching_done) - - 'status_error' → Error → НЕ анімуємо (залишається в списку) - """ - if widget := self.file_items.get(path): - widget.update_status(status, self.translator.get(text_key)) - - # ← НОВЕ: НЕ видаляємо файли під час обробки - # Вони будуть видалені в patching_done - # Це запобігає подвійному видаленню - - def patching_done(self, stats: Tuple[int, int, int], was_cancelled: bool, cancelled_file_index: int = None, total_files: int = None, remaining_files: int = None): - """ - Обробляє завершення патчингу. - """ - s, k, e = stats - - if total_files is None: - total_files = cancelled_file_index + len(self.files) if cancelled_file_index else len(self.files) - - if remaining_files is None: - remaining_files = total_files - cancelled_file_index if cancelled_file_index is not None else 0 - - # ================================================================ - # PARTE 0: Скидаємо UI стан - # ================================================================ - self.progress.setValue(0) - self.set_ui_for_patching(False) - - # ================================================================ - # PARTE 1: Якщо було скасування - видаляємо обробленні файли - # ================================================================ - if was_cancelled and cancelled_file_index is not None: - # ← ВАЖЛИВО: Видаляємо ВСІХ файлів від 0 до cancelled_file_index - # включаючи ті що мають статус 'cancelled' - paths_to_remove = [self.files[i]['path'] for i in range(cancelled_file_index)] - - for path in paths_to_remove: - if path in self.file_items: - widget = self.file_items[path] - # Видаляємо з layout - self.files_layout.removeWidget(widget) - widget.deleteLater() - self.file_items.pop(path, None) - - # Оновлюємо список файлів (залишаємо тільки необроблені) - self.files = self.files[cancelled_file_index:] - - # Перевіряємо чи список пустий - if not self.files: - self.files_container.hide() - self.empty_state.show() - - # Оновлюємо статистику - self.update_stats() - - # Логуємо з правильною кількістю залишених файлів - self.log("log_patched_files_removed", "info", [cancelled_file_index, total_files, remaining_files]) - else: - # Якщо не було скасування, але всі файли обробленні - if not was_cancelled: - for path in list(self.file_items.keys()): - widget = self.file_items[path] - self.files_layout.removeWidget(widget) - widget.deleteLater() - self.file_items.pop(path, None) - - self.files.clear() - self.files_container.hide() - self.empty_state.show() - self.update_stats() - - # ================================================================ - # PARTE 2: Формуємо повідомлення про результати - # ================================================================ - summary_parts = [] - - if s > 0: - summary_parts.append(self.translator.get("summary_patched", s)) - if k > 0: - summary_parts.append(self.translator.get("summary_skipped", k)) - if e > 0: - summary_parts.append(self.translator.get("summary_errors", e)) - - if was_cancelled: - summary_text = self.translator.get("summary_cancelled_prefix") + ", ".join(summary_parts) - else: - summary_text = (self.translator.get("summary_finished_prefix") + ", ".join(summary_parts)) if summary_parts else self.translator.get("summary_no_ops") - - level = "success" if e == 0 and s > 0 and not was_cancelled else "warning" - self.log(summary_text, level, []) - - # ================================================================ - # PARTE 3: Показуємо діалог з затримкою - # ================================================================ - if not was_cancelled: - QMessageBox.information( - self, - self.translator.get("dialog_completed_title"), - summary_text - ) - else: - self.log("log_operation_stopped", "info") - - def show_cancel_dialog(): - if remaining_files > 0: - cancel_message = self.translator.get("dialog_cancel_remaining", remaining_files) - QMessageBox.information( - self, - self.translator.get("dialog_cancelled_title"), - cancel_message - ) - - QTimer.singleShot(500, show_cancel_dialog) - - def show_about(self): - dialog = AboutDialog(self, self.translator) - dialog.exec() - -# ============================================================================= -# 6. ENTRY POINT -# ============================================================================= -if __name__ == '__main__': - app = QApplication(sys.argv) - app.setStyle('Fusion') - app.setStyleSheet(REFINED_STYLESHEET) - - lang_code, show_dialog = load_settings() - - if show_dialog: - dialog = LanguageDialog() - if dialog.exec() == QDialog.DialogCode.Accepted: - lang_code, show_dialog_next_time = dialog.get_selection() - save_settings(lang_code, show_dialog_next_time) - else: sys.exit(0) - - translator = TranslationManager(lang_code) - - qt_translator = QTranslator() - if lang_code != "en": - translations_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath) - if qt_translator.load(f"qt_{lang_code}.qm", translations_path): - app.installTranslator(qt_translator) - - window = PEPatcherGUI(translator) - window.show() - sys.exit(app.exec()) +# -*- coding: utf-8 -*- +""" +PE Patcher GUI — graphical tool for binary-patching DLL import names in PE files. + +Patches Windows PE executables to redirect DLL imports (WINHTTP, WININET, etc.) +using both IAT-level and raw-hex strategies. Supports backup, overwrite, folder +scanning, and a localised PySide6 dark-theme UI. +""" +# pylint: disable=line-too-long +# pylint: disable=too-many-lines +# pylint: disable=too-many-instance-attributes +# pylint: disable=too-few-public-methods +# pylint: disable=too-many-public-methods +# pylint: disable=too-many-statements +# pylint: disable=too-many-locals +# pylint: disable=too-many-branches +# pylint: disable=too-many-nested-blocks +# pylint: disable=too-many-arguments +# pylint: disable=too-many-positional-arguments +# pylint: disable=attribute-defined-outside-init +# pylint: disable=redefined-outer-name +# pylint: disable=invalid-name +# pylint: disable=c-extension-no-member + +import sys +import os +import stat +import shutil +import re +from datetime import datetime +from pathlib import Path +from typing import List, Tuple +from configparser import ConfigParser +import glob + +import defusedxml.ElementTree as ET # pylint: disable=import-error +import lief # pylint: disable=import-error +from PySide6.QtWidgets import ( # pylint: disable=no-name-in-module + QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, + QLabel, QTextEdit, QFrame, QFileDialog, QMessageBox, QCheckBox, QDialog, + QProgressBar, QScrollArea, QGraphicsDropShadowEffect, + QGroupBox, QSplitter, QTextBrowser, QMenu, +) +from PySide6.QtCore import ( # pylint: disable=no-name-in-module + QThread, QObject, Signal, Qt, QTranslator, QLibraryInfo, QTimer, + QPropertyAnimation, QPoint, QEasingCurve, QParallelAnimationGroup, +) +from PySide6.QtGui import QColor, QFont, QPalette # pylint: disable=no-name-in-module + +try: + from config import DLL_REPLACEMENTS +except ImportError: + print("❌ Error: config.py file not found or is corrupted") + sys.exit(1) +except Exception as exc: # pylint: disable=broad-exception-caught + print(f"❌ Error loading configuration: {exc}") + sys.exit(1) + +DONATION_ADDRESSES = { + 'bitcoin': 'bc1pfnf3ukjn6sdpdujwxav8wlv0p6k5sp5fzwnz8wmdndd57z9yym7slu5dgr', + 'ethereum': '0x671c0f7d78d777da2b576ca9b6cc559f7e048d5f', + 'monero': '43myvYnEM8q2g1AULm7dp1XzLRrjZ73VaSnCmvyhSEHHGG1e3weAUFG8RWZhSasbSz9H8jZpGv8LQ8wc9aQHjvfSKW4rt4z', + 'ton': 'UQCb_q_NLHfYC4Sj0MURw57mYlK6IQXSpOkzBZIyyXnscp7m', + 'usdt_trc20': 'TFqV65zvK6NfPbtmx1pqVxSYBCjW8Vz23K', + 'usdt_erc20': '0x671c0f7d78d777da2b576ca9b6cc559f7e048d5f', + 'usdc_erc20': '0x671c0f7d78d777da2b576ca9b6cc559f7e048d5f', + 'tron': 'TTFqV65zvK6NfPbtmx1pqVxSYBCjW8Vz23K', + 'bnb': '0x671c0f7d78d777da2b576ca9b6cc559f7e048d5f', + 'github': 'https://github.com/EXLOUD', +} + +APP_VERSION = "1.0.13" +LANG_FOLDER = "languages" + + +def sanitize_filename(filename: str) -> str: + """Replace characters forbidden in Windows filenames with underscores.""" + return re.sub(r'[\\/*?:"<>|]', '_', filename) + + +def resource_path(relative_path): + """Return the absolute path to a resource, handling PyInstaller bundles.""" + base_path = getattr(sys, '_MEIPASS', os.path.abspath(".")) + return os.path.join(base_path, relative_path) + + +def get_base_path(): + """Return the directory that contains the running executable or script.""" + if getattr(sys, 'frozen', False): + return os.path.dirname(sys.executable) + return os.path.dirname(os.path.abspath(__file__)) + + +# ============================================================================= +# THEME +# ============================================================================= +REFINED_PALETTE = { + 'bg_primary': '#0E0E10', 'bg_secondary': '#141417', 'bg_tertiary': '#1A1A1E', + 'bg_elevated': '#202024', 'bg_overlay': '#26262B', + 'accent': '#8B7FB8', 'accent_hover': '#9D91C7', + 'accent_muted': 'rgba(139, 127, 184, 0.15)', 'accent_subtle': 'rgba(139, 127, 184, 0.08)', + 'text': '#E8E6F0', 'text_secondary': '#A8A5B8', 'text_muted': '#6B6878', + 'text_disabled': '#48465A', + 'success': '#6BCF7F', 'warning': '#E4A853', 'error': '#CF6679', 'info': '#64B5F6', + 'border': 'rgba(255, 255, 255, 0.04)', 'border_hover': 'rgba(139, 127, 184, 0.2)', + 'shadow': 'rgba(0, 0, 0, 0.4)', +} + +STANDARD_BUTTON_STYLE = f""" + QPushButton {{ + font-size: 13px; font-weight: 500; letter-spacing: 0.5px; padding: 11px 20px; + border-radius: 8px; background-color: {REFINED_PALETTE['bg_tertiary']}; + color: {REFINED_PALETTE['text_secondary']}; border: none; + }} + QPushButton:hover {{ + background-color: {REFINED_PALETTE['bg_overlay']}; color: {REFINED_PALETTE['text']}; + }} + QPushButton:disabled {{ + background-color: {REFINED_PALETTE['bg_overlay']}; color: {REFINED_PALETTE['text_muted']}; + }} +""" + +REFINED_STYLESHEET = f""" + * {{ margin: 0; padding: 0; border: none; outline: none; }} + QWidget {{ color: {REFINED_PALETTE['text']}; font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui, sans-serif; font-size: 13px; font-weight: 400; letter-spacing: 0.3px; }} + QMainWindow {{ background-color: {REFINED_PALETTE['bg_primary']}; }} + #RefinedCard {{ background-color: {REFINED_PALETTE['bg_secondary']}; border-radius: 12px; }} + #ElevatedCard {{ background-color: {REFINED_PALETTE['bg_elevated']}; border-radius: 16px; border: 1px solid {REFINED_PALETTE['border']}; }} + #AppHeader {{ background-color: {REFINED_PALETTE['bg_secondary']}; border-bottom: 1px solid {REFINED_PALETTE['border']}; padding: 28px 32px; }} + {STANDARD_BUTTON_STYLE} + QPushButton[variant="primary"] {{ background-color: {REFINED_PALETTE['accent']}; color: white; font-weight: 600; }} + QPushButton[variant="primary"]:hover {{ background-color: {REFINED_PALETTE['accent_hover']}; }} + QPushButton[variant="primary"]:disabled {{ background-color: {REFINED_PALETTE['accent_muted']}; color: rgba(255, 255, 255, 0.5); }} + QPushButton[variant="secondary"] {{ background-color: {REFINED_PALETTE['accent_subtle']}; color: {REFINED_PALETTE['accent']}; border: 1px solid {REFINED_PALETTE['accent_muted']}; }} + QPushButton[variant="secondary"]:hover {{ background-color: {REFINED_PALETTE['accent_muted']}; border-color: {REFINED_PALETTE['accent']}; }} + QPushButton[variant="ghost"] {{ background-color: transparent; color: {REFINED_PALETTE['text_muted']}; padding: 8px 12px; }} + QPushButton[variant="ghost"]:hover {{ background-color: {REFINED_PALETTE['bg_overlay']}; color: {REFINED_PALETTE['text_secondary']}; }} + QLabel {{ background-color: transparent; }} + QLabel[class="h1"] {{ font-size: 32px; font-weight: 300; letter-spacing: -0.5px; color: {REFINED_PALETTE['text']}; }} + QLabel[class="h3"] {{ font-size: 18px; font-weight: 500; color: {REFINED_PALETTE['text']}; }} + #FileItem {{ background-color: {REFINED_PALETTE['bg_tertiary']}; border-radius: 0; padding: 14px 16px; border: 1px solid transparent; }} + #FileItem:hover {{ background-color: {REFINED_PALETTE['bg_overlay']}; border-color: {REFINED_PALETTE['border_hover']}; }} + #Divider {{ background-color: {REFINED_PALETTE['border']}; height: 1px; margin: 10px 0; }} + QGroupBox {{ background-color: transparent; border: 1px solid {REFINED_PALETTE['border']}; border-radius: 8px; padding-top: 16px; font-size: 11px; font-weight: 600; letter-spacing: 0.5px; text-transform: uppercase; }} + QGroupBox::title {{ subcontrol-origin: margin; left: 12px; padding: 0 8px; color: {REFINED_PALETTE['text_muted']}; background-color: {REFINED_PALETTE['bg_secondary']}; text-align: center; }} + QTextEdit {{ background-color: {REFINED_PALETTE['bg_tertiary']}; border: 1px solid {REFINED_PALETTE['border']}; border-radius: 8px; padding: 12px; font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; font-size: 12px; line-height: 1.6; color: {REFINED_PALETTE['text_secondary']}; }} + QProgressBar {{ background-color: {REFINED_PALETTE['bg_overlay']}; height: 5px; border-radius: 5px; border: none; text-align: center; margin: 0px; padding: 0px; }} + QProgressBar::chunk {{ background-color: {REFINED_PALETTE['accent']}; border-radius: 5px; margin: 0px; padding: 0px; }} + QScrollArea QScrollBar:vertical {{ background-color: transparent; width: 16px; margin: 20px 4px 20px 4px; }} + QScrollBar:vertical {{ background-color: {REFINED_PALETTE['bg_overlay']}; width: 12px; border-radius: 6px; margin: 4px 2px; }} + QScrollBar::handle:vertical {{ background-color: {REFINED_PALETTE['text_muted']}; border-radius: 6px; min-height: 50px; margin: 2px; width: 12px; }} + QScrollBar::handle:vertical:hover {{ background-color: {REFINED_PALETTE['accent']}; width: 12px; }} + QScrollBar::handle:vertical:pressed {{ background-color: {REFINED_PALETTE['accent_hover']}; }} + QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical, QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {{ height: 0; background: transparent; }} + QScrollArea QScrollBar:horizontal {{ background-color: transparent; height: 16px; margin: 4px 20px 4px 20px; }} + QScrollBar:horizontal {{ background-color: {REFINED_PALETTE['bg_overlay']}; height: 12px; border-radius: 6px; margin: 2px 4px; }} + QScrollBar::handle:horizontal {{ background-color: {REFINED_PALETTE['text_muted']}; border-radius: 6px; min-width: 50px; margin: 2px; height: 12px; }} + QScrollBar::handle:horizontal:hover {{ background-color: {REFINED_PALETTE['accent']}; height: 12px; }} + QScrollBar::handle:horizontal:pressed {{ background-color: {REFINED_PALETTE['accent_hover']}; }} + QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal, QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {{ width: 0; background: transparent; }} + QAbstractScrollArea::corner {{ background-color: transparent; }} + QDialog, QMessageBox {{ background-color: {REFINED_PALETTE['bg_secondary']}; }} + QMessageBox QLabel {{ color: {REFINED_PALETTE['text']}; qproperty-alignment: AlignCenter; }} + QMenu {{ + background-color: {REFINED_PALETTE['bg_elevated']}; + color: {REFINED_PALETTE['text']}; + border: 1px solid {REFINED_PALETTE['border_hover']}; + border-radius: 8px; + padding: 4px; + }} + QMenu::item {{ + padding: 8px 20px 8px 12px; + border-radius: 4px; + background-color: transparent; + }} + QMenu::item:selected {{ + background-color: {REFINED_PALETTE['bg_overlay']}; + color: {REFINED_PALETTE['text']}; + }} + QMenu::item:disabled {{ + color: {REFINED_PALETTE['text_disabled']}; + }} + QMenu::separator {{ + height: 1px; + background-color: {REFINED_PALETTE['border']}; + margin: 4px 8px; + }} +""" + + +class DarkMenu(QMenu): + """QMenu subclass with WA_TranslucentBackground for truly transparent rounded corners on Linux.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + self.setWindowFlags( + self.windowFlags() + | Qt.WindowType.FramelessWindowHint + | Qt.WindowType.NoDropShadowWindowHint + ) + + +class CenteredMessageBox(QMessageBox): + """QMessageBox subclass that centres the message text both horizontally and vertically.""" + + def showEvent(self, event): + """Center-align the text label every time the dialog is shown.""" + super().showEvent(event) + for label in self.findChildren(QLabel): + # Skip the icon label (it has a pixmap, not text) + if label.text(): + label.setAlignment( + Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter + ) + label.setMinimumWidth(label.sizeHint().width()) + + +def _msg(icon, parent, title, text): + """Create, configure, and exec a CenteredMessageBox; return the result.""" + box = CenteredMessageBox(icon, title, text, QMessageBox.StandardButton.Ok, parent) + box.exec() + + +def _msg_question(parent, title, text): + """Show a Yes/No CenteredMessageBox and return True if Yes was clicked.""" + box = CenteredMessageBox( + QMessageBox.Icon.Question, title, text, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + parent, + ) + return box.exec() == QMessageBox.StandardButton.Yes + + +class FolderSearchDialog(QDialog): + """Dialog asking whether to search subfolders recursively. + + Buttons stretch to fill the full dialog width and the whole window is + resizable, matching the main application style. + """ + + # Result constants + RECURSIVE = 1 + FOLDER_ONLY = 2 + CANCELLED = 3 + + def __init__(self, parent, translator): + """Build the dialog layout.""" + super().__init__(parent) + self.translator = translator + self.result_choice = self.CANCELLED + self.setWindowTitle(translator.get("dialog_search_option")) + self.setMinimumWidth(380) + self.setSizeGripEnabled(True) + + layout = QVBoxLayout(self) + layout.setContentsMargins(28, 24, 28, 24) + layout.setSpacing(20) + + text = QLabel(translator.get("dialog_search_subfolders")) + text.setWordWrap(True) + text.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter) + text.setStyleSheet(f"color: {REFINED_PALETTE['text']}; font-size: 13px;") + layout.addWidget(text) + + btn_layout = QHBoxLayout() + btn_layout.setSpacing(10) + + yes_btn = QPushButton(translator.get("dialog_yes_recursive")) + yes_btn.setProperty("variant", "primary") + yes_btn.setSizePolicy(yes_btn.sizePolicy().horizontalPolicy(), + yes_btn.sizePolicy().verticalPolicy()) + yes_btn.clicked.connect(self._on_yes) + + no_btn = QPushButton(translator.get("dialog_no_folder_only")) + no_btn.clicked.connect(self._on_no) + + cancel_btn = QPushButton(translator.get("dialog_cancel")) + cancel_btn.clicked.connect(self._on_cancel) + + for btn in (yes_btn, no_btn, cancel_btn): + btn.setMinimumHeight(40) + btn_layout.addWidget(btn, 1) # stretch factor 1 → equal width + + layout.addLayout(btn_layout) + + def _on_yes(self): + self.result_choice = self.RECURSIVE + self.accept() + + def _on_no(self): + self.result_choice = self.FOLDER_ONLY + self.accept() + + def _on_cancel(self): + self.result_choice = self.CANCELLED + self.reject() + + +def create_subtle_shadow(): + """Create and return a soft drop-shadow graphics effect for elevated cards.""" + shadow = QGraphicsDropShadowEffect() + shadow.setBlurRadius(16) + shadow.setXOffset(0) + shadow.setYOffset(2) + shadow.setColor(QColor(0, 0, 0, 80)) + return shadow + + +# ============================================================================= +# TRANSLATIONS & SETTINGS +# ============================================================================= +class TranslationManager: + """Loads XML translation files and provides key-based string lookup.""" + + def __init__(self, lang_code='en'): + """Initialise and immediately load the requested language.""" + self.translations = {} + self.load_language(lang_code) + + def load_language(self, lang_code): + """Parse *lang_.xml* and populate the translation map.""" + lang_path = resource_path(LANG_FOLDER) + if not os.path.exists(lang_path): + print(f"⚠️ Warning: Languages folder not found: {lang_path}") + self.translations = {} + return + filepath = os.path.join(lang_path, f"lang_{lang_code}.xml") + if not os.path.exists(filepath): + print(f"⚠️ Warning: Translation file not found: {filepath}") + self.translations = {} + return + try: + tree = ET.parse(filepath) + root = tree.getroot() + for string_tag in root.findall('string'): + key = string_tag.get('name') + value = string_tag.text + if key and value: + self.translations[key] = value + except Exception as exc: # pylint: disable=broad-exception-caught + print(f"❌ Error loading translation file {filepath}: {exc}") + self.translations = {} + + def get(self, key, *args): + """Return the translated string for *key*, formatted with *args*.""" + template = self.translations.get(key, key) + try: + return template.format(*args) + except (IndexError, TypeError): + return template + + +def get_settings_path(): + """Return the absolute path to settings.ini next to the executable.""" + return os.path.join(get_base_path(), "settings.ini") + + +def load_settings(): + """Load language code and show-dialog flag; return defaults when missing.""" + path = get_settings_path() + config = ConfigParser() + if os.path.exists(path): + config.read(path, encoding='utf-8') + return ( + config.get("Settings", "language", fallback="en"), + config.getboolean("Settings", "show_dialog", fallback=True), + ) + return "en", True + + +def save_settings(lang, show_dialog): + """Persist language code and show-dialog flag to settings.ini.""" + path = get_settings_path() + config = ConfigParser() + config.add_section("Settings") + config.set("Settings", "language", lang) + config.set("Settings", "show_dialog", str(show_dialog)) + with open(path, 'w', encoding='utf-8') as f: + config.write(f) + + +def get_language_name_from_xml(filepath: str) -> str: + """Extract the human-readable language name from a translation XML file.""" + try: + tree = ET.parse(filepath) + root = tree.getroot() + lang_name_tag = root.find(".//string[@name='language_name']") + if lang_name_tag is not None and lang_name_tag.text: + return lang_name_tag.text + except Exception as exc: # pylint: disable=broad-exception-caught + print(f"\u26a0\ufe0f Warning: could not read language name from {filepath}: {exc}") + basename = os.path.basename(filepath) + try: + return basename.split('_')[1].split('.')[0] + except IndexError: + return basename + + +# ============================================================================= +# BACKEND LOGIC +# ============================================================================= +class PatcherLogEmitter(QObject): + """Emits translated log messages as Qt signals for cross-thread logging.""" + + log_signal = Signal(str, str, list) + + def emit(self, key: str, level: str, args: list = None): + """Emit a log signal with translation key, severity level and format args.""" + self.log_signal.emit(key, level, args or []) + + +class PermissionsManager: + """Context manager that temporarily grants write permission to a read-only file.""" + + def __init__(self, file_path: str, log_emitter: PatcherLogEmitter): + """Store path and emitter; initialise state flags.""" + self.file_path = file_path + self.log_emitter = log_emitter + self.original_permissions = None + self.permissions_were_changed = False + + def __enter__(self): + """Ensure the file is writable, changing permissions if necessary.""" + try: + self.original_permissions = os.stat(self.file_path).st_mode + if not self.original_permissions & stat.S_IWUSR: + self.log_emitter.emit("log_readonly_file", "warning", + [os.path.basename(self.file_path)]) + self.log_emitter.emit("log_changing_perms", "info") + os.chmod(self.file_path, self.original_permissions | stat.S_IWUSR) + self.log_emitter.emit("log_perms_changed", "success") + self.permissions_were_changed = True + return self + except Exception as exc: # pylint: disable=broad-exception-caught + self.log_emitter.emit("log_perms_error", "error", [str(exc)]) + raise + + def __exit__(self, exc_type, exc_val, exc_tb): + """Restore the original file permissions when leaving the context.""" + if self.permissions_were_changed and self.original_permissions is not None: + try: + self.log_emitter.emit("log_restoring_perms", "info") + os.chmod(self.file_path, self.original_permissions) + self.log_emitter.emit("log_perms_restored", "success") + except Exception as exc: # pylint: disable=broad-exception-caught + self.log_emitter.emit("log_restore_perms_error", "error", [str(exc)]) + + +class UniversalPEPatcher: + """Patches DLL names inside a PE file using both IAT and raw-hex strategies.""" + + def __init__(self, file_path: str, selected_apis: List[int], + log_emitter: PatcherLogEmitter): + """Build the replacement map from the selected API numbers.""" + self.file_path = file_path + self.log_emitter = log_emitter + self.binary = None + self.data = None + self.active_replacements = { + k: v + for api_num in selected_apis + for k, v in DLL_REPLACEMENTS[api_num]['replacements'].items() + } + + def _rva_to_offset(self, rva: int): + """Convert a relative virtual address to a raw file offset.""" + for section in self.binary.sections: + vstart = section.virtual_address + vsize = max(section.virtual_size, section.size) + if vstart <= rva < vstart + vsize: + return section.offset + (rva - vstart) + return None + + def load_file(self) -> bool: + """Read the file into a mutable bytearray and parse it with LIEF.""" + try: + with open(self.file_path, 'rb') as f: + self.data = bytearray(f.read()) + self.binary = lief.PE.parse(self.file_path) + if self.binary is None: + raise ValueError("LIEF: could not parse file as PE") + return True + except Exception as exc: # pylint: disable=broad-exception-caught + self.log_emitter.emit("log_load_error", "error", [str(exc)]) + return False + + def check_if_patchable(self) -> int: + """Return the number of replaceable occurrences found in this PE.""" + if not self.data and not self.load_file(): + return 0 + count = 0 + iat_details = [] + hex_details = [] + + if self.binary and self.binary.has_imports: + for imp in self.binary.imports: + dll_upper = imp.name.upper() + for orig_bytes in self.active_replacements: + if dll_upper == orig_bytes.decode('utf-8', 'ignore').rstrip('\x00').upper(): + count += 1 + iat_details.append(dll_upper) + break + + for old_bytes in self.active_replacements: + hex_count = self.data.count(old_bytes) + if hex_count > 0: + count += hex_count + hex_details.append( + f"{old_bytes.decode('utf-8', 'ignore')} x{hex_count}" + ) + + if iat_details or hex_details: + self.log_emitter.emit("log_patch_details_header", "info") + if iat_details: + self.log_emitter.emit("log_patch_details_iat", "info", + [', '.join(iat_details)]) + if hex_details: + self.log_emitter.emit("log_patch_details_hex", "info", + [', '.join(hex_details)]) + return count + + def patch_all(self) -> int: + """Apply IAT and hex patches; return the total number of replacements made.""" + count = 0 + iat_count = 0 + hex_patched_details = {} + + try: + import_dir = self.binary.data_directories[1] + dir_rva = import_dir.rva + dir_file_offset = self._rva_to_offset(dir_rva) if dir_rva else None + + if dir_file_offset is not None: + idx = 0 + while True: + base = dir_file_offset + idx * 20 + if base + 20 > len(self.data): + break + oft = int.from_bytes(self.data[base:base + 4], 'little') + name_rva = int.from_bytes(self.data[base + 12:base + 16], 'little') + ft = int.from_bytes(self.data[base + 16:base + 20], 'little') + if oft == 0 and ft == 0: + break + if name_rva: + name_off = self._rva_to_offset(name_rva) + if name_off is not None and name_off < len(self.data): + end = name_off + while end < len(self.data) and self.data[end] != 0: + end += 1 + dll_name = self.data[name_off:end].decode('utf-8', 'ignore') + for orig, repl in self.active_replacements.items(): + orig_str = orig.decode('utf-8', 'ignore').rstrip('\x00').upper() + if dll_name.upper() == orig_str: + avail = end - name_off + 1 + if len(repl) <= avail: + self.data[name_off:name_off + len(repl)] = repl + if len(repl) < avail: + pad = avail - len(repl) + self.data[ + name_off + len(repl): + name_off + len(repl) + pad + ] = b'\x00' * pad + count += 1 + iat_count += 1 + else: + self.log_emitter.emit( + "log_iat_skipped_long", "warning", [dll_name] + ) + break + idx += 1 + except Exception as exc: # pylint: disable=broad-exception-caught + self.log_emitter.emit("log_iat_parse_failed", "warning", [str(exc)]) + + for old, new in self.active_replacements.items(): + if len(old) != len(new): + self.log_emitter.emit( + "log_hex_skipped_len", "warning", [old.decode('utf-8', 'ignore')] + ) + continue + start_index = 0 + local_count = 0 + while (index := self.data.find(old, start_index)) != -1: + self.data[index:index + len(old)] = new + start_index = index + len(old) + count += 1 + local_count += 1 + if local_count > 0: + hex_patched_details[old.decode('utf-8', 'ignore')] = ( + new.decode('utf-8', 'ignore'), local_count + ) + + if count > 0: + for dll, (new_dll, cnt) in hex_patched_details.items(): + self.log_emitter.emit("log_hex_patched", "info", [dll, new_dll, cnt]) + self.log_emitter.emit("log_total_changes", "success", [count]) + return count + + def save(self, output_path: str) -> bool: + """Write the patched byte buffer to *output_path*.""" + try: + with open(output_path, 'wb') as f: + f.write(bytes(self.data)) + self.log_emitter.emit( + "log_file_saved", "success", [os.path.basename(output_path)] + ) + return True + except Exception as exc: # pylint: disable=broad-exception-caught + self.log_emitter.emit("log_save_error", "error", [str(exc)]) + return False + + def close(self): + """Release LIEF binary and byte buffer.""" + self.binary = None + self.data = None + + +class FileProcessorWorker(QObject): + """Background worker that validates and collects metadata for a list of PE files.""" + + file_processed = Signal(dict) + finished = Signal(int, int) + + def __init__(self, file_paths): + """Store the list of paths to process.""" + super().__init__() + self.file_paths = file_paths + + @staticmethod + def _read_pe_info(path): + """Extract PE type and architecture by reading raw header bytes only. + + Avoids a full lief.PE.parse call — reads just enough bytes to locate + the COFF header and inspect machine type and characteristics. + Returns (type_str, arch_str) or ('PE', 'x86') on any read error. + """ + try: + with open(path, 'rb') as f: + dos = f.read(64) + if len(dos) < 64 or dos[:2] != b'MZ': + return 'PE', 'x86' + pe_offset = int.from_bytes(dos[60:64], 'little') + f.seek(pe_offset) + coff = f.read(24) # PE\0\0 + 20-byte COFF header + if len(coff) < 24 or coff[:4] != b'PE\x00\x00': + return 'PE', 'x86' + machine = int.from_bytes(coff[4:6], 'little') + characteristics = int.from_bytes(coff[22:24], 'little') + arch = 'x64' if machine == 0x8664 else ('arm64' if machine == 0xAA64 else 'x86') + if characteristics & 0x2000: + pe_type = 'DLL' + elif characteristics & 0x0002: + pe_type = 'EXE' + else: + pe_type = 'PE' + return pe_type, arch + except Exception: # pylint: disable=broad-exception-caught + return 'PE', 'x86' + + def run(self): + """Validate each file as a PE and emit metadata; emit totals when done.""" + added = 0 + error = 0 + for path in self.file_paths: + try: + pe_type, arch = self._read_pe_info(path) + if pe_type == 'PE' and arch == 'x86': + # Verify MZ signature was valid (read_pe_info returns defaults on error) + try: + with open(path, 'rb') as f: + if f.read(2) != b'MZ': + error += 1 + continue + except Exception: # pylint: disable=broad-exception-caught + error += 1 + continue + info = { + 'path': path, + 'size': os.path.getsize(path), + 'type': pe_type, + 'arch': arch, + } + self.file_processed.emit(info) + added += 1 + except Exception: # pylint: disable=broad-exception-caught + error += 1 + self.finished.emit(added, error) + + +class FolderScannerWorker(QObject): + """Background worker that recursively scans a folder for PE files.""" + + finished = Signal() + file_found = Signal(str) + scan_complete = Signal(list, list) + + def __init__(self, folder_path, include_subfolders): + """Store scan parameters.""" + super().__init__() + self.folder_path = Path(folder_path) + self.include_subfolders = include_subfolders + self.is_cancelled = False + + def run(self): + """Scan the folder and emit each discovered PE file name.""" + try: + found = [] + skipped = set() + exts = {'.exe', '.dll', '.vst3', '.vst', '.sys', '.ocx', '.ax'} + pattern = '**/*' if self.include_subfolders else '*' + for p in self.folder_path.glob(pattern): + if self.is_cancelled: + break + if p.is_dir() or p.suffix.lower() not in exts: + continue + parts_lower = {part.lower() for part in p.parts} + if not {'patched', 'backup'}.isdisjoint(parts_lower): + skipped.add("Patched/Backup") + continue + try: + with p.open('rb') as f: + if f.read(2) == b'MZ': + found.append(str(p)) + self.file_found.emit(p.name) + except (IOError, PermissionError): + continue + if not self.is_cancelled: + self.scan_complete.emit(sorted(list(set(found))), sorted(list(skipped))) + except Exception as exc: # pylint: disable=broad-exception-caught + print(f"Error in FolderScannerWorker: {exc}") + finally: + self.finished.emit() + + def cancel(self): + """Request cancellation of the ongoing scan.""" + self.is_cancelled = True + + +class PatcherWorker(QObject): + """Background worker that applies DLL-name patches to a batch of PE files.""" + + log_message = Signal(str, str, list) + file_status_updated = Signal(str, str, str) + progress_updated = Signal(int) + finished = Signal(tuple, bool, int, int, int) # cancelled_file_index uses -1 instead of None + + def __init__(self, files, selected_apis, backup, overwrite): + """Store all patching parameters.""" + super().__init__() + self.files = files + self.selected_apis = selected_apis + self.backup_var = backup + self.overwrite_var = overwrite + self.is_cancelled = False + self.total_files = len(files) + self.log_emitter = PatcherLogEmitter() + self.log_emitter.log_signal.connect(self.log_message) + + def cancel(self): + """Request cancellation after the current file finishes.""" + self.log_message.emit("log_cancel_request", "warning", []) + self.is_cancelled = True + + def run(self): + """Iterate over files, patch each one, and emit progress/status signals.""" + s = 0 + e = 0 + k = 0 + total = len(self.files) + was_cancelled = False + cancelled_file_index = None + + try: + for i, info in enumerate(self.files): + if self.is_cancelled: + was_cancelled = True + cancelled_file_index = i + self.log_message.emit("log_patching_cancelled", "warning", []) + for remaining_info in self.files[i:]: + self.file_status_updated.emit( + remaining_info['path'], 'warning', 'status_cancelled' + ) + break + + path = info['path'] + name = sanitize_filename(os.path.basename(path)) + self.log_message.emit("", "info", []) + self.log_message.emit( + "log_processing_file", "info", + [f"[{i + 1}/{total}]", os.path.basename(path)] + ) + original_file_data = None + + try: + with open(path, 'rb') as fh: + original_file_data = fh.read() + except Exception as read_err: # pylint: disable=broad-exception-caught + self.log_message.emit("log_read_error", "error", [str(read_err)]) + e += 1 + self.file_status_updated.emit(path, 'error', 'status_error') + self.progress_updated.emit(int((i + 1) / total * 100)) + continue + + patcher = None + try: + with PermissionsManager(path, self.log_emitter): + if self.is_cancelled: + was_cancelled = True + cancelled_file_index = i + self.log_message.emit("log_patching_cancelled", "warning", []) + for remaining_info in self.files[i:]: + self.file_status_updated.emit( + remaining_info['path'], 'warning', 'status_cancelled' + ) + break + + patcher = UniversalPEPatcher(path, self.selected_apis, self.log_emitter) + if not patcher.load_file() or patcher.check_if_patchable() == 0: + self.log_message.emit("log_nothing_to_patch", "warning", []) + k += 1 + self.file_status_updated.emit(path, 'warning', 'status_skipped') + self.progress_updated.emit(int((i + 1) / total * 100)) + continue + + if self.is_cancelled: + was_cancelled = True + cancelled_file_index = i + self.log_message.emit("log_patching_cancelled", "warning", []) + for remaining_info in self.files[i:]: + self.file_status_updated.emit( + remaining_info['path'], 'warning', 'status_cancelled' + ) + break + + p_count = patcher.patch_all() + + if p_count > 0: + if self.is_cancelled: + was_cancelled = True + cancelled_file_index = i + self.log_message.emit("log_patching_cancelled", "warning", []) + for remaining_info in self.files[i:]: + self.file_status_updated.emit( + remaining_info['path'], 'warning', 'status_cancelled' + ) + break + + if self.backup_var: + b_dir = os.path.join(os.path.dirname(path), 'backup') + os.makedirs(b_dir, exist_ok=True) + b_path = os.path.join(b_dir, name) + cnt = 1 + base, ext = os.path.splitext(name) + while os.path.exists(b_path): + b_path = os.path.join(b_dir, f"{base}.backup{cnt}{ext}") + cnt += 1 + with open(b_path, 'wb') as bf: + bf.write(original_file_data) + self.log_message.emit( + "log_backup_saved", "info", [os.path.basename(b_path)] + ) + + if self.is_cancelled: + was_cancelled = True + cancelled_file_index = i + self.log_message.emit("log_patching_cancelled", "warning", []) + for remaining_info in self.files[i:]: + self.file_status_updated.emit( + remaining_info['path'], 'warning', 'status_cancelled' + ) + break + + p_dir = os.path.join(os.path.dirname(path), 'patched') + os.makedirs(p_dir, exist_ok=True) + i_path = os.path.join(p_dir, name) + + if not patcher.save(i_path): + e += 1 + self.file_status_updated.emit(path, 'error', 'status_error') + self.progress_updated.emit(int((i + 1) / total * 100)) + continue + + if self.overwrite_var: + try: + shutil.move(i_path, path) + self.log_message.emit("log_original_replaced", "info", []) + if not os.listdir(p_dir): + os.rmdir(p_dir) + except Exception as move_err: # pylint: disable=broad-exception-caught + self.log_message.emit( + "log_move_error", "error", [str(move_err)] + ) + e += 1 + self.file_status_updated.emit(path, 'error', 'status_error') + + s += 1 + self.file_status_updated.emit(path, 'success', 'status_done') + else: + k += 1 + self.file_status_updated.emit(path, 'warning', 'status_no_changes') + + except Exception as err: # pylint: disable=broad-exception-caught + self.log_message.emit("log_general_error", "error", [str(err)]) + e += 1 + self.file_status_updated.emit(path, 'error', 'status_error') + finally: + if patcher: + patcher.close() + + self.progress_updated.emit(int((i + 1) / total * 100)) + + finally: + remaining_files = ( + total - cancelled_file_index + if cancelled_file_index is not None else 0 + ) + self.finished.emit( + (s, k, e), + was_cancelled, + cancelled_file_index if cancelled_file_index is not None else -1, + self.total_files, + remaining_files, + ) + + +class ThreadManager(QObject): + """Manages a single background QThread, ensuring only one task runs at a time.""" + + task_started = Signal(str) + task_finished = Signal(str) + error = Signal(str, str) + + def __init__(self, parent=None): + """Initialise with empty thread/worker references.""" + super().__init__(parent) + self.current_thread = None + self.current_worker = None + self.current_task_name = None + + def is_running(self) -> bool: + """Return True if a background thread is currently active.""" + return self.current_thread is not None and self.current_thread.isRunning() + + def start_task(self, worker_class, task_name: str, *args, **kwargs) -> QObject: + """Create and start a worker of *worker_class*; return the worker instance.""" + if self.is_running(): + self.error.emit("dialog_op_in_progress", "") + return None + self.current_task_name = task_name + self.current_thread = QThread() + self.current_worker = worker_class(*args, **kwargs) + worker = self.current_worker + worker.moveToThread(self.current_thread) + self.current_thread.started.connect(worker.run) + worker.finished.connect(self.current_thread.quit) + self.current_thread.finished.connect(self._cleanup_after_thread_finish) + self.current_thread.finished.connect(worker.deleteLater) + self.current_thread.finished.connect(self.current_thread.deleteLater) + self.current_thread.start() + self.task_started.emit(task_name) + return worker + + def _cleanup_after_thread_finish(self): + """Reset internal references after a thread completes.""" + task_name = self.current_task_name + self.current_thread = None + self.current_worker = None + self.current_task_name = None + self.task_finished.emit(task_name) + + def stop_current_task(self): + """Request cancellation of the running worker if it supports cancel().""" + if self.is_running() and hasattr(self.current_worker, 'cancel'): + self.current_worker.cancel() + + +# ============================================================================= +# WIDGETS & DIALOGS +# ============================================================================= +class LanguageDialog(QDialog): + """Startup dialog that lets the user pick the UI language from available XML files.""" + + def __init__(self, parent=None): + """Build the language-selection UI.""" + super().__init__(parent) + self.setWindowTitle("Language Selection") + self.setMinimumWidth(400) + self.setMinimumHeight(300) + self.language = "en" + + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(24, 24, 24, 24) + main_layout.setSpacing(20) + + title_label = QLabel("Select Language") + title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + title_label.setProperty("class", "h3") + main_layout.addWidget(title_label) + + lang_path = resource_path(LANG_FOLDER) + lang_files = ( + glob.glob(os.path.join(lang_path, "lang_*.xml")) + if os.path.exists(lang_path) else [] + ) + + if not lang_files: + error_label = QLabel( + "⚠️ Language files not found!\n\n" + "Please ensure the 'languages' folder exists with translation files:\n" + "- languages/lang_en.xml\n\n" + "Copy the language files from the application directory and try again." + ) + error_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + error_label.setWordWrap(True) + error_label.setStyleSheet( + f"color: {REFINED_PALETTE['warning']}; font-size: 12px;" + ) + main_layout.addWidget(error_label, 1) + + close_btn = QPushButton("Exit") + close_btn.setStyleSheet(STANDARD_BUTTON_STYLE) + close_btn.clicked.connect(self.reject) + btn_layout = QHBoxLayout() + btn_layout.addStretch() + btn_layout.addWidget(close_btn) + btn_layout.addStretch() + main_layout.addLayout(btn_layout) + else: + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setFrameShape(QFrame.Shape.NoFrame) + scroll_area.setStyleSheet("background: transparent;") + + button_container = QWidget() + buttons_layout = QVBoxLayout(button_container) + buttons_layout.setContentsMargins(0, 0, 0, 0) + buttons_layout.setSpacing(8) + + available_languages = {} + for lang_file in lang_files: + lang_code = os.path.basename(lang_file).split('_')[1].split('.')[0] + lang_name = get_language_name_from_xml(lang_file) + available_languages[lang_code] = lang_name + + for code, name in sorted(available_languages.items()): + btn = QPushButton(name) + btn.setStyleSheet(STANDARD_BUTTON_STYLE) + btn.clicked.connect(lambda _, c=code: self.set_language(c)) + buttons_layout.addWidget(btn) + + buttons_layout.addStretch() + button_container.setLayout(buttons_layout) + scroll_area.setWidget(button_container) + main_layout.addWidget(scroll_area) + + self.show_again_checkbox = QCheckBox("Show every time") + self.show_again_checkbox.setChecked(True) + self.show_again_checkbox.setStyleSheet(f""" + QCheckBox {{ spacing: 8px; color: {REFINED_PALETTE['text_secondary']}; }} + QCheckBox::indicator {{ + width: 16px; height: 16px; border-radius: 4px; + border: 1px solid {REFINED_PALETTE['text_muted']}; + background-color: {REFINED_PALETTE['bg_overlay']}; + }} + QCheckBox::indicator:hover {{ + border-color: {REFINED_PALETTE['accent']}; + }} + QCheckBox::indicator:checked {{ + background-color: {REFINED_PALETTE['accent']}; + border-color: {REFINED_PALETTE['accent']}; + image: url(none); + }} + QCheckBox::indicator:checked:hover {{ + background-color: {REFINED_PALETTE['accent_hover']}; + border-color: {REFINED_PALETTE['accent_hover']}; + }} + """) + main_layout.addWidget(self.show_again_checkbox) + + def set_language(self, lang_code): + """Store the selected language code and close the dialog.""" + self.language = lang_code + self.accept() + + def get_selection(self): + """Return (language_code, show_again_flag) from the dialog.""" + if hasattr(self, 'show_again_checkbox'): + return self.language, self.show_again_checkbox.isChecked() + return self.language, False + + +class RefinedContainer(QWidget): + """Styled QWidget used as a card or elevated-card container in the UI.""" + + def __init__(self, container_type="card", parent=None): + """Create the container with the correct object name and optional shadow.""" + super().__init__(parent) + name_map = {"card": "RefinedCard", "elevated": "ElevatedCard"} + self.setObjectName(name_map.get(container_type, "RefinedCard")) + self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) + if container_type == "elevated": + self.setGraphicsEffect(create_subtle_shadow()) + + +class SwipeableFileItem(QWidget): + """File list row widget that supports a left-swipe gesture to trigger removal.""" + + removed = Signal(str) + + def __init__(self, file_info, translator): + """Build the file row UI with name, details, status and remove button.""" + super().__init__() + self.file_info = file_info + self.translator = translator + self.start_pos = None + self.current_pos = 0 + self.swipe_threshold = 60 + self.is_swiped = False + + wrapper_layout = QVBoxLayout(self) + wrapper_layout.setContentsMargins(0, 0, 0, 0) + wrapper_layout.setSpacing(0) + + file_widget = QWidget() + file_widget.setObjectName("FileItem") + file_widget.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) + file_widget.setFixedHeight(56) + + main_layout = QHBoxLayout(file_widget) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + + self.content_widget = QWidget() + self.content_widget.setStyleSheet("background: transparent;") + + content_layout = QHBoxLayout(self.content_widget) + content_layout.setContentsMargins(16, 0, 16, 0) + content_layout.setSpacing(12) + + info_layout = QVBoxLayout() + info_layout.setSpacing(2) + info_layout.setContentsMargins(0, 0, 0, 0) + + name_label = QLabel(os.path.basename(file_info['path'])) + name_label.setProperty("class", "subtitle") + name_label.setStyleSheet(f"color: {REFINED_PALETTE['text']};") + info_layout.addWidget(name_label) + + details = QLabel( + f"{file_info['type']} · {self._format_size(file_info['size'])} · {file_info['arch']}" + ) + details.setProperty("class", "caption") + info_layout.addWidget(details) + content_layout.addLayout(info_layout, 1) + + self.status_label = QLabel( + self.translator.get(file_info.get('status_text_key', 'status_ready')) + ) + self.status_label.setProperty("class", "caption") + content_layout.addWidget(self.status_label) + + remove_btn = QPushButton("×") + remove_btn.setProperty("variant", "ghost") + remove_btn.setFixedSize(24, 24) + remove_btn.setStyleSheet( + "QPushButton { font-size: 18px; padding: 0; border-radius: 4px; color: #6B6878; }" + " QPushButton:hover { color: #CF6679; background-color: rgba(207,102,121,0.1); }" + ) + remove_btn.setCursor(Qt.CursorShape.PointingHandCursor) + remove_btn.clicked.connect(lambda: self.removed.emit(self.file_info['path'])) + content_layout.addWidget(remove_btn) + + self.delete_icon = QLabel("🗑️") + self.delete_icon.setProperty("class", "caption") + self.delete_icon.setStyleSheet( + f"color: {REFINED_PALETTE['error']}; padding: 0 16px;" + ) + self.delete_icon.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.delete_icon.hide() + + main_layout.addWidget(self.content_widget) + main_layout.addWidget(self.delete_icon) + + self.animation = QPropertyAnimation(self.content_widget, b"pos") + self.animation.setDuration(200) + self.animation.finished.connect(self.on_animation_finished) + + wrapper_layout.addWidget(file_widget) + + divider = QFrame() + divider.setFixedHeight(1) + divider.setStyleSheet( + f"background-color: {REFINED_PALETTE['border']}; margin: 0;" + ) + wrapper_layout.addWidget(divider) + + def _format_size(self, size): + """Convert *size* bytes to a human-readable string.""" + for unit in ['B', 'KB', 'MB', 'GB']: + if size < 1024.0: + return f"{size:.1f}{unit}" + size /= 1024.0 + return f"{size:.1f}TB" + + def mousePressEvent(self, event): + """Record the press position to track horizontal swipe distance.""" + if event.button() == Qt.MouseButton.LeftButton: + self.start_pos = event.pos() + + def mouseMoveEvent(self, event): + """Slide the content widget left as the user drags to show the delete icon.""" + if self.start_pos is not None and event.buttons() & Qt.MouseButton.LeftButton: + delta = event.pos().x() - self.start_pos.x() + if delta < 0: + self.current_pos = max(delta, -self.swipe_threshold) + self.content_widget.move(self.current_pos, 0) + if abs(self.current_pos) > self.swipe_threshold * 0.7: + self.delete_icon.show() + else: + self.delete_icon.hide() + + def mouseReleaseEvent(self, _event): + """Commit the swipe-removal or spring back, depending on distance.""" + if self.start_pos is not None: + if abs(self.current_pos) > self.swipe_threshold * 0.8: + self.is_swiped = True + self.animation.setStartValue(self.content_widget.pos()) + self.animation.setEndValue(QPoint(-self.width(), 0)) + self.animation.start() + else: + self.animation.setStartValue(self.content_widget.pos()) + self.animation.setEndValue(QPoint(0, 0)) + self.animation.start() + self.delete_icon.hide() + self.start_pos = None + + def on_animation_finished(self): + """Emit *removed* signal when the swipe-out animation completes.""" + if self.is_swiped: + self.removed.emit(self.file_info['path']) + + def update_status(self, status: str, text: str): + """Update the status label text and colour.""" + self.status_label.setText(text) + colors = { + 'success': REFINED_PALETTE['success'], + 'warning': REFINED_PALETTE['warning'], + 'error': REFINED_PALETTE['error'], + 'ready': REFINED_PALETTE['text_muted'], + } + self.status_label.setStyleSheet(f"color: {colors.get(status, colors['ready'])};") + + +class RefinedFolderDialog(QDialog): + """Dialog that scans a folder for PE files and lets the user review them.""" + + files_changed = Signal() + + def update_display(self): + """Refresh the file count label and button state after a removal.""" + file_count = len(self.found_files) + if file_count == 0: + self.scroll_area.hide() + self.empty_state_internal.show() + self.status_label.setText(self.translator.get("files_not_added")) + else: + self.empty_state_internal.hide() + self.scroll_area.show() + self.status_label.setText(self.translator.get("found_n_files", file_count)) + self.update_buttons(is_scanning=False, found_count=file_count) + + def __init__(self, parent, folder_path, include_subfolders): + """Build the scan dialog and launch the background scanner thread.""" + super().__init__(parent) + self.found_files = [] + self.is_closing = False + self.translator = parent.translator + self.setWindowTitle(self.translator.get("scan_folder_title")) + self.setFixedSize(600, 640) + self.setModal(True) + + layout = QVBoxLayout(self) + layout.setContentsMargins(24, 24, 24, 24) + layout.setSpacing(16) + + header_widget = QWidget() + header_widget.setStyleSheet( + f"background-color: {REFINED_PALETTE['bg_secondary']};" + " border-radius: 12px; padding: 16px;" + ) + header_layout = QVBoxLayout(header_widget) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.setSpacing(12) + + title_label = QLabel(self.translator.get("scan_folder_title")) + title_label.setProperty("class", "h3") + title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + header_layout.addWidget(title_label) + + path_scroll = QScrollArea() + path_scroll.setWidgetResizable(True) + path_scroll.setFixedHeight(80) + path_scroll.setFrameShape(QFrame.Shape.NoFrame) + path_scroll.setStyleSheet( + f"QScrollArea {{ background-color: {REFINED_PALETTE['bg_tertiary']};" + f" border: none; border-radius: 8px; }}" + f" QScrollBar:horizontal {{ background-color: {REFINED_PALETTE['bg_overlay']};" + f" height: 8px; border-radius: 4px; margin: 2px; }}" + f" QScrollBar::handle:horizontal {{ background-color: {REFINED_PALETTE['text_muted']};" + f" border-radius: 4px; min-width: 50px; margin: 1px; }}" + f" QScrollBar::handle:horizontal:hover {{ background-color: {REFINED_PALETTE['accent']}; }}" + " QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal" + " { width: 0; background: transparent; }" + ) + + path_label = QLabel(folder_path) + path_label.setProperty("class", "mono") + path_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + path_label.setStyleSheet( + f"color: {REFINED_PALETTE['text_secondary']}; padding: 12px;" + ) + path_scroll.setWidget(path_label) + header_layout.addWidget(path_scroll) + layout.addWidget(header_widget) + + self.status_label = QLabel(self.translator.get("scanning_status_searching")) + self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(self.status_label) + + self.central_container = QWidget() + self.central_layout = QVBoxLayout(self.central_container) + self.central_layout.setContentsMargins(0, 0, 0, 0) + self.central_layout.setSpacing(0) + + empty_scroll = QScrollArea() + empty_scroll.setWidgetResizable(True) + empty_scroll.setFrameShape(QFrame.Shape.NoFrame) + empty_scroll.setStyleSheet("background: transparent;") + + self.empty_state = RefinedContainer("elevated") + self.empty_state.setStyleSheet( + f"background-color: {REFINED_PALETTE['bg_secondary']}; border-radius: 12px;" + ) + empty_layout = QVBoxLayout(self.empty_state) + empty_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + empty_layout.setSpacing(12) + empty_layout.setContentsMargins(24, 24, 24, 24) + + self.empty_text = QLabel(self.translator.get("files_not_added")) + self.empty_text.setProperty("class", "h3") + self.empty_text.setAlignment(Qt.AlignmentFlag.AlignCenter) + empty_layout.addWidget(self.empty_text) + + empty_scroll.setWidget(self.empty_state) + self.central_layout.addWidget(empty_scroll, 1) + + self.files_container = RefinedContainer("elevated") + self.files_container.setStyleSheet( + f"background-color: {REFINED_PALETTE['bg_tertiary']};" + " border: none; border-radius: 8px;" + ) + self.files_layout = QVBoxLayout(self.files_container) + self.files_layout.setContentsMargins(12, 12, 12, 12) + self.files_layout.setSpacing(0) + + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setFrameShape(QFrame.Shape.NoFrame) + scroll.setStyleSheet("background: transparent; border: none;") + scroll.hide() + + self.files_content = QWidget() + self.files_content.setStyleSheet("background: transparent;") + self.files_main_layout = QVBoxLayout(self.files_content) + self.files_main_layout.setContentsMargins(0, 0, 0, 0) + self.files_main_layout.setSpacing(0) + self.files_main_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + scroll.setWidget(self.files_content) + self.files_layout.addWidget(scroll) + self.scroll_area = scroll + + self.empty_state_internal = QWidget() + self.empty_state_internal.setStyleSheet("background: transparent;") + empty_internal_layout = QVBoxLayout(self.empty_state_internal) + empty_internal_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + empty_internal_layout.setSpacing(12) + + empty_internal_text = QLabel(self.translator.get("files_not_added")) + empty_internal_text.setProperty("class", "h3") + empty_internal_text.setAlignment(Qt.AlignmentFlag.AlignCenter) + empty_internal_text.setStyleSheet(f"color: {REFINED_PALETTE['text_muted']};") + empty_internal_layout.addWidget(empty_internal_text) + + self.files_layout.addWidget(self.empty_state_internal) + self.empty_state_internal.hide() + self.central_layout.addWidget(self.files_container, 1) + layout.addWidget(self.central_container, 1) + + self.info_container = QWidget() + self.info_container.setStyleSheet( + f"background-color: {REFINED_PALETTE['bg_tertiary']};" + " border-radius: 8px; padding: 12px;" + ) + info_layout = QVBoxLayout(self.info_container) + info_layout.setContentsMargins(12, 12, 12, 12) + info_layout.setSpacing(6) + + self.skipped_label = QLabel() + self.skipped_label.setProperty("class", "caption") + self.skipped_label.setStyleSheet(f"color: {REFINED_PALETTE['text_muted']};") + self.skipped_label.setWordWrap(True) + info_layout.addWidget(self.skipped_label) + self.info_container.hide() + layout.addWidget(self.info_container) + + self.progress_bar = QProgressBar() + self.progress_bar.setRange(0, 0) + self.progress_bar.setTextVisible(False) + self.progress_bar.setFixedHeight(3) + layout.addWidget(self.progress_bar) + + self.btn_layout = QHBoxLayout() + self.btn_layout.setSpacing(12) + layout.addLayout(self.btn_layout) + self.update_buttons(is_scanning=True) + + self.thread = QThread(self) + self.worker = FolderScannerWorker(folder_path, include_subfolders) + self.worker.moveToThread(self.thread) + + self.thread.started.connect(self.worker.run) + self.worker.finished.connect(self.thread.quit) + self.thread.finished.connect(self.on_thread_finished) + self.worker.scan_complete.connect(self.on_scan_complete) + self.worker.file_found.connect(self.on_file_found) + + self.thread.start() + self.files_changed.connect(self.update_display) + + def update_buttons(self, is_scanning=False, found_count=0): + """Rebuild the button row for scanning vs. finished states.""" + while self.btn_layout.count(): + item = self.btn_layout.takeAt(0) + if item.widget(): + item.widget().deleteLater() + + self.btn_layout.addStretch() + if is_scanning: + cancel_btn = QPushButton(self.translator.get("cancel")) + cancel_btn.setStyleSheet(STANDARD_BUTTON_STYLE) + cancel_btn.clicked.connect(self.reject) + self.btn_layout.addWidget(cancel_btn) + else: + if found_count > 0: + add_btn = QPushButton(self.translator.get("add_n_files", found_count)) + add_btn.setStyleSheet(STANDARD_BUTTON_STYLE) + add_btn.setProperty("variant", "primary") + add_btn.clicked.connect(self.accept) + self.btn_layout.addWidget(add_btn) + close_btn = QPushButton(self.translator.get("close")) + close_btn.setStyleSheet(STANDARD_BUTTON_STYLE) + close_btn.clicked.connect(self.reject) + self.btn_layout.addWidget(close_btn) + self.btn_layout.addStretch() + + def on_file_found(self, filename): + """Add a newly discovered file to the list while scanning.""" + file_widget = QWidget() + file_layout = QHBoxLayout(file_widget) + file_layout.setContentsMargins(12, 8, 12, 8) + file_layout.setSpacing(12) + + file_item = QLabel(filename) + file_item.setProperty("class", "caption") + file_item.setStyleSheet(f"color: {REFINED_PALETTE['text_secondary']};") + file_item.setWordWrap(True) + file_layout.addWidget(file_item, 1) + + remove_btn = QPushButton("×") + remove_btn.setProperty("variant", "ghost") + remove_btn.setFixedSize(24, 24) + remove_btn.setStyleSheet( + "QPushButton { font-size: 18px; padding: 0; border-radius: 4px; color: #6B6878; }" + " QPushButton:hover { color: #CF6679; background-color: rgba(207,102,121,0.1); }" + ) + remove_btn.setCursor(Qt.CursorShape.PointingHandCursor) + remove_btn.clicked.connect(lambda: self.remove_file_item(file_widget)) + file_layout.addWidget(remove_btn) + + file_widget.setStyleSheet( + "QWidget { background-color: transparent; border-radius: 6px; padding: 0px; }" + ) + file_widget.enterEvent = lambda ev: self.on_file_item_hover(file_widget, True) + file_widget.leaveEvent = lambda ev: self.on_file_item_hover(file_widget, False) + + self.files_main_layout.addWidget(file_widget) + + if self.files_main_layout.count() > 1: + divider = QFrame() + divider.setFrameShape(QFrame.Shape.HLine) + divider.setFrameShadow(QFrame.Shadow.Plain) + divider.setLineWidth(1) + divider.setFixedHeight(1) + divider.setStyleSheet( + f"QFrame {{ background-color: {REFINED_PALETTE['border']};" + f" margin: 0; border: none; }}" + ) + self.files_main_layout.insertWidget(self.files_main_layout.count() - 1, divider) + + if self.files_main_layout.count() <= 2: + for idx in range(self.central_layout.count()): + widget = self.central_layout.itemAt(idx).widget() + if isinstance(widget, QScrollArea) and widget.widget() == self.empty_state: + widget.hide() + self.scroll_area.show() + + def on_file_item_hover(self, widget, is_hovering): + """Highlight or reset a file row on mouse enter/leave.""" + if is_hovering: + widget.setStyleSheet( + f"QWidget {{ background-color: {REFINED_PALETTE['bg_overlay']};" + f" border-radius: 6px; padding: 0px; }}" + ) + else: + widget.setStyleSheet( + "QWidget { background-color: transparent; border-radius: 6px; padding: 0px; }" + ) + + def remove_file_item(self, widget): + """Remove a file row from the list and update the found-files state.""" + index = self.files_main_layout.indexOf(widget) + if index >= 0: + self.files_main_layout.removeWidget(widget) + widget.deleteLater() + + if index < self.files_main_layout.count(): + next_widget = self.files_main_layout.itemAt(index).widget() + if isinstance(next_widget, QFrame): + self.files_main_layout.removeWidget(next_widget) + next_widget.deleteLater() + elif index > 0: + prev_widget = self.files_main_layout.itemAt(index - 1).widget() + if isinstance(prev_widget, QFrame): + self.files_main_layout.removeWidget(prev_widget) + prev_widget.deleteLater() + + self.found_files = [] + for idx in range(self.files_main_layout.count()): + widget_item = self.files_main_layout.itemAt(idx).widget() + if not isinstance(widget_item, QFrame): + for j in range(widget_item.layout().count()): + child = widget_item.layout().itemAt(j).widget() + next_item = ( + widget_item.layout().itemAt(j + 1).widget() + if j + 1 < widget_item.layout().count() else None + ) + if isinstance(child, QLabel) and child != next_item: + self.found_files.append(child.text()) + break + + self.files_changed.emit() + + def on_scan_complete(self, found, skipped): + """Handle scanner completion: show results and update buttons.""" + self.found_files = found + self.progress_bar.setRange(0, 100) + self.progress_bar.setValue(100) + + if skipped: + self.skipped_label.setText( + self.translator.get("skipped_folders_info") + ", ".join(skipped) + ) + self.info_container.show() + + if found: + self.status_label.setText(self.translator.get("found_n_files", len(found))) + else: + self.scroll_area.hide() + for idx in range(self.central_layout.count()): + widget = self.central_layout.itemAt(idx).widget() + if isinstance(widget, QScrollArea) and widget.widget() == self.empty_state: + widget.show() + self.status_label.setText(self.translator.get("files_not_added")) + + self.update_buttons(is_scanning=False, found_count=len(found)) + + def on_thread_finished(self): + """Clean up thread and worker references after the scan ends.""" + if self.thread: + self.thread.deleteLater() + if self.worker: + self.worker.deleteLater() + self.thread = None + self.worker = None + if self.is_closing: + super().reject() + + def get_found_files(self): + """Return the list of discovered PE file paths.""" + return self.found_files + + def reject(self): + """Cancel the scan gracefully before closing if it is still running.""" + if self.thread and self.thread.isRunning(): + if not self.is_closing: + self.is_closing = True + self.status_label.setText(self.translator.get("cancelling")) + for idx in range(self.btn_layout.count()): + item = self.btn_layout.itemAt(idx) + if item: + btn_widget = item.widget() + if btn_widget: + btn_widget.setEnabled(False) + self.worker.cancel() + else: + super().reject() + + def closeEvent(self, event): + """Intercept the window close to ensure the scan is cancelled first.""" + event.ignore() + self.reject() + + +class RefinedSplitter(QSplitter): + """QSplitter with a wider, transparent handle styled for the dark theme.""" + + def __init__(self, orientation, parent=None): + """Create the splitter with a wide transparent handle.""" + super().__init__(orientation, parent) + self.setHandleWidth(20) + self.setStyleSheet("QSplitter { background-color: transparent; }") + + +class RefinedDivider(QFrame): + """Thin horizontal rule used as a visual separator inside panels.""" + + def __init__(self, parent=None): + """Create a 1-pixel styled horizontal line.""" + super().__init__(parent) + self.setFrameShape(QFrame.Shape.HLine) + self.setFrameShadow(QFrame.Shadow.Plain) + self.setLineWidth(1) + self.setFixedHeight(1) + self.setStyleSheet( + f"QFrame {{ background-color: {REFINED_PALETTE['border']};" + f" margin: 6px 0; border: none; }}" + ) + + +class AboutDialog(QDialog): + """Dialog showing application version, author link and donation addresses.""" + + def __init__(self, parent, translator): + """Initialise and build the About dialog.""" + super().__init__(parent) + self.translator = translator + self.setWindowTitle(self.translator.get("about")) + self.setMinimumSize(600, 690) + self.setWindowFlags( + self.windowFlags() + | Qt.WindowType.WindowMaximizeButtonHint + ) + self.setup_ui() + + def setup_ui(self): + """Construct all UI widgets for the About dialog.""" + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(24, 24, 24, 24) + main_layout.setSpacing(16) + + about_text = ( + f"

{self.translator.get('app_title')} {APP_VERSION}

" + f"
" + f"

" + f"{self.translator.get('author')}:" + f" " + f"github.com/EXLOUD

" + ) + + text_browser = QTextBrowser() + text_browser.setHtml(about_text) + text_browser.setReadOnly(True) + text_browser.setOpenExternalLinks(True) + text_browser.setFixedHeight(120) + + donations_panel = QWidget() + donations_panel_layout = QVBoxLayout(donations_panel) + donations_panel_layout.setContentsMargins(0, 16, 0, 0) + donations_panel_layout.setSpacing(8) + + title_label = QLabel(self.translator.get("donation_title")) + title_label.setProperty("class", "caption") + donations_panel_layout.addWidget(title_label) + + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_area.setFrameShape(QFrame.Shape.NoFrame) + scroll_area.setStyleSheet("background: transparent;") + + button_container = QWidget() + buttons_layout = QVBoxLayout(button_container) + buttons_layout.setContentsMargins(0, 8, 0, 0) + buttons_layout.setSpacing(8) + + address_buttons = [ + ("Bitcoin", "bitcoin"), ("Ethereum", "ethereum"), ("Monero", "monero"), + ("TON", "ton"), ("USDT (TRC20)", "usdt_trc20"), ("USDT (ERC20)", "usdt_erc20"), + ("USDC (ERC20)", "usdc_erc20"), ("Tron", "tron"), ("BNB", "bnb"), + ] + for btn_name, key in address_buttons: + btn = QPushButton(f"📋 {btn_name}") + btn.setStyleSheet(STANDARD_BUTTON_STYLE) + btn.setCursor(Qt.CursorShape.PointingHandCursor) + btn.clicked.connect( + lambda checked, addr_key=key: ( + QApplication.clipboard().setText(DONATION_ADDRESSES[addr_key]), + self.parent().log( + "log_copied", "success", + [addr_key.upper(), DONATION_ADDRESSES[addr_key][:15]] + ), + ) + ) + buttons_layout.addWidget(btn) + + buttons_layout.addStretch() + button_container.setLayout(buttons_layout) + scroll_area.setWidget(button_container) + donations_panel_layout.addWidget(scroll_area) + + splitter = QSplitter(Qt.Orientation.Vertical) + splitter.addWidget(text_browser) + splitter.addWidget(donations_panel) + splitter.setSizes([120, 520]) + splitter.setStyleSheet( + "QSplitter::handle { height: 1px; background-color: transparent; }" + ) + main_layout.addWidget(splitter) + + close_btn = QPushButton(self.translator.get("close")) + close_btn.setStyleSheet(STANDARD_BUTTON_STYLE) + close_btn.setFixedWidth(120) + close_btn.clicked.connect(self.accept) + + btn_layout = QHBoxLayout() + btn_layout.addStretch() + btn_layout.addWidget(close_btn) + btn_layout.addStretch() + main_layout.addLayout(btn_layout) + + +# ============================================================================= +# MAIN WINDOW +# ============================================================================= +class PEPatcherGUI(QMainWindow): + """Main application window for the PE Patcher tool.""" + + def __init__(self, translator: TranslationManager): + """Initialise the main window, build the UI, and log the startup message.""" + super().__init__() + self.translator = translator + self.files = [] + self.file_items = {} + self.thread_manager = ThreadManager(self) + self.thread_manager.error.connect(self.show_task_error) + self.setMinimumSize(960, 780) + self.center_window() + self.setup_ui() + self.retranslate_ui() + self.log("log_app_started", "info", + [self.translator.get('app_title'), APP_VERSION]) + + def center_window(self): + """Move the window to the centre of the available screen area.""" + screen = self.screen() + if screen: + geo = screen.availableGeometry() + self.move( + (geo.width() - self.width()) // 2, + (geo.height() - self.height()) // 2, + ) + + def setup_ui(self): + """Build the full main-window layout with header, panels, and footer.""" + main = QWidget() + self.setCentralWidget(main) + layout = QVBoxLayout(main) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + self._create_header(layout) + + content = QWidget() + content_layout = QVBoxLayout(content) + content_layout.setContentsMargins(24, 24, 24, 24) + content_layout.setSpacing(0) + + self.horizontal_splitter = RefinedSplitter(Qt.Orientation.Horizontal) + left_panel = self._create_left_panel() + self.horizontal_splitter.addWidget(left_panel) + + self.vertical_splitter = RefinedSplitter(Qt.Orientation.Vertical) + settings_panel = self._create_settings_panel() + log_panel = self._create_log_panel() + self.vertical_splitter.addWidget(settings_panel) + self.vertical_splitter.addWidget(log_panel) + self.vertical_splitter.setSizes([300, 300]) + + self.horizontal_splitter.addWidget(self.vertical_splitter) + self.horizontal_splitter.setSizes([600, 400]) + content_layout.addWidget(self.horizontal_splitter) + layout.addWidget(content, 1) + self._create_bottom(layout) + + def _create_header(self, layout): + """Build and add the top header widget with title and file counter.""" + header = QWidget() + header.setObjectName("AppHeader") + header_layout = QHBoxLayout(header) + + title_section = QWidget() + title_layout = QVBoxLayout(title_section) + title_layout.setContentsMargins(0, 0, 0, 0) + title_layout.setSpacing(2) + + self.title_label = QLabel() + self.title_label.setProperty("class", "h1") + title_layout.addWidget(self.title_label) + + self.subtitle_label = QLabel() + self.subtitle_label.setProperty("class", "caption") + title_layout.addWidget(self.subtitle_label) + + header_layout.addWidget(title_section) + header_layout.addStretch() + + stats_section = QWidget() + stats_layout = QHBoxLayout(stats_section) + stats_layout.setSpacing(24) + + self.files_count = QLabel("0") + self.files_count.setProperty("class", "h1") + self.files_count.setStyleSheet(f"color: {REFINED_PALETTE['accent']};") + stats_layout.addWidget(self.files_count) + + self.files_label = QLabel() + self.files_label.setProperty("class", "caption") + stats_layout.addWidget(self.files_label) + + header_layout.addWidget(stats_section) + layout.addWidget(header) + + def _create_left_panel(self): + """Build the left panel with file-add buttons, file list, and action buttons.""" + container = QWidget() + layout = QVBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(12) + + file_actions_container = RefinedContainer("card") + file_actions_layout = QVBoxLayout(file_actions_container) + file_actions_layout.setContentsMargins(20, 16, 20, 16) + file_actions_layout.setSpacing(12) + + header_layout_top = QHBoxLayout() + header_layout_top.setSpacing(12) + + self.add_files_btn = QPushButton() + self.add_files_btn.clicked.connect(self.add_files) + header_layout_top.addWidget(self.add_files_btn, 1) + + self.add_folder_btn = QPushButton() + self.add_folder_btn.clicked.connect(self.add_folder) + header_layout_top.addWidget(self.add_folder_btn, 1) + + file_actions_layout.addLayout(header_layout_top) + layout.addWidget(file_actions_container) + + files_panel = self._create_files_panel() + layout.addWidget(files_panel, 1) + + buttons_container = RefinedContainer("card") + buttons_layout = QHBoxLayout(buttons_container) + buttons_layout.setContentsMargins(20, 16, 20, 16) + buttons_layout.setSpacing(12) + + self.main_action_btn = QPushButton() + self.main_action_btn.clicked.connect(self.on_main_action_click) + buttons_layout.addWidget(self.main_action_btn, 1) + + self.clear_btn = QPushButton() + self.clear_btn.clicked.connect(self.clear_all) + buttons_layout.addWidget(self.clear_btn, 1) + + layout.addWidget(buttons_container) + return container + + def _create_files_panel(self): + """Build the scrollable file list panel with empty state.""" + container = RefinedContainer("elevated") + layout = QVBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setFrameShape(QFrame.Shape.NoFrame) + scroll.setStyleSheet("background: transparent;") + + self.files_content = QWidget() + self.files_content.setStyleSheet("background: transparent;") + self.files_main_layout = QVBoxLayout(self.files_content) + self.files_main_layout.setContentsMargins(0, 0, 0, 0) + self.files_main_layout.setSpacing(0) + + self.empty_state = QWidget() + self.empty_state.setObjectName("EmptyState") + self.empty_state.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) + self.empty_state.setMinimumHeight(250) + + empty_layout = QVBoxLayout(self.empty_state) + empty_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + empty_layout.setSpacing(12) + + self.empty_text = QLabel() + self.empty_text.setProperty("class", "h3") + self.empty_text.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.empty_hint = QLabel() + self.empty_hint.setProperty("class", "caption") + self.empty_hint.setAlignment(Qt.AlignmentFlag.AlignCenter) + + empty_layout.addWidget(self.empty_text) + empty_layout.addWidget(self.empty_hint) + + self.files_container = QWidget() + self.files_container.setStyleSheet("background: transparent;") + self.files_layout = QVBoxLayout(self.files_container) + self.files_layout.setContentsMargins(12, 12, 12, 12) + self.files_layout.setSpacing(0) + self.files_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + self.files_main_layout.addWidget(self.empty_state) + self.files_main_layout.addWidget(self.files_container) + self.files_container.hide() + + scroll.setWidget(self.files_content) + layout.addWidget(scroll, 1) + return container + + def _create_settings_panel(self): + """Build the right-top settings panel with API checkboxes and options.""" + settings = RefinedContainer("card") + settings_layout = QVBoxLayout(settings) + settings_layout.setContentsMargins(0, 0, 0, 0) + settings_layout.setSpacing(0) + + header_widget = QWidget() + header_widget.setStyleSheet( + f"background-color: {REFINED_PALETTE['bg_secondary']};" + " border-top-left-radius: 12px; border-top-right-radius: 12px;" + " padding-bottom: 10px;" + ) + header_layout = QVBoxLayout(header_widget) + header_layout.setContentsMargins(20, 12, 20, 0) + header_layout.setSpacing(0) + + self.settings_title = QLabel() + self.settings_title.setProperty("class", "h3") + self.settings_title.setAlignment(Qt.AlignmentFlag.AlignCenter) + header_layout.addWidget(self.settings_title) + settings_layout.addWidget(header_widget) + + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setFrameShape(QFrame.Shape.NoFrame) + scroll.setStyleSheet("background: transparent;") + + settings_content = QWidget() + content_layout = QVBoxLayout(settings_content) + content_layout.setContentsMargins(20, 20, 20, 20) + content_layout.setSpacing(16) + + self.api_group = QGroupBox() + api_layout = QVBoxLayout(self.api_group) + api_layout.setContentsMargins(12, 12, 12, 12) + api_layout.setSpacing(12) + + self.all_apis = QCheckBox() + self.all_apis.setChecked(True) + self.all_apis.stateChanged.connect(self.toggle_apis) + api_layout.addWidget(self.all_apis) + api_layout.addWidget(RefinedDivider()) + + self.api_checks = {} + for num, data in DLL_REPLACEMENTS.items(): + check = QCheckBox(data['name']) + check.setChecked(True) + check.stateChanged.connect(self.on_api_change) + self.api_checks[num] = check + api_layout.addWidget(check) + + api_layout.addStretch() + content_layout.addWidget(self.api_group) + + self.options_group = QGroupBox() + options_layout = QVBoxLayout(self.options_group) + options_layout.setContentsMargins(12, 12, 12, 12) + options_layout.setSpacing(8) + + self.backup = QCheckBox() + self.backup.setChecked(True) + options_layout.addWidget(self.backup) + + self.overwrite = QCheckBox() + self.overwrite.setChecked(True) + options_layout.addWidget(self.overwrite) + + content_layout.addWidget(self.options_group) + content_layout.addStretch() + + scroll.setWidget(settings_content) + settings_layout.addWidget(scroll, 1) + return settings + + def _create_log_panel(self): + """Build the right-bottom log panel with a scrollable text area.""" + log_panel = RefinedContainer("card") + log_layout = QVBoxLayout(log_panel) + log_layout.setContentsMargins(0, 0, 0, 0) + log_layout.setSpacing(0) + + header_widget = QWidget() + header_widget.setStyleSheet( + f"background-color: {REFINED_PALETTE['bg_secondary']};" + " border-top-left-radius: 12px; border-top-right-radius: 12px;" + " padding-bottom: 10px;" + ) + header_layout = QHBoxLayout(header_widget) + header_layout.setContentsMargins(20, 12, 20, 0) + header_layout.setSpacing(12) + + self.log_title = QLabel() + self.log_title.setProperty("class", "h3") + header_layout.addWidget(self.log_title) + header_layout.addStretch() + + self.clear_log_btn = QPushButton() + self.clear_log_btn.setProperty("variant", "ghost") + self.clear_log_btn.clicked.connect(self.clear_log) + header_layout.addWidget(self.clear_log_btn) + log_layout.addWidget(header_widget) + + self.log_text = QTextEdit() + self.log_text.setReadOnly(True) + self.log_text.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.log_text.customContextMenuRequested.connect(self._show_log_context_menu) + + # Keep selection highlight blue (Active color) even when widget loses focus + _palette = self.log_text.palette() + _hl_color = _palette.color(QPalette.ColorGroup.Active, QPalette.ColorRole.Highlight) + _hl_text = _palette.color(QPalette.ColorGroup.Active, QPalette.ColorRole.HighlightedText) + _palette.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.Highlight, _hl_color) + _palette.setColor(QPalette.ColorGroup.Inactive, QPalette.ColorRole.HighlightedText, _hl_text) + self.log_text.setPalette(_palette) + + self.log_text.setStyleSheet( + f"QTextEdit {{ padding: 20px; border: none; border-radius: 0;" + f" border-bottom-left-radius: 12px; border-bottom-right-radius: 12px;" + f" background-color: {REFINED_PALETTE['bg_tertiary']};" + " font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;" + " font-size: 12px; }" + ) + log_layout.addWidget(self.log_text, 1) + return log_panel + + def _create_bottom(self, layout): + """Build and add the bottom bar with progress indicator and About button.""" + bottom = QWidget() + bottom.setStyleSheet( + f"background: {REFINED_PALETTE['bg_secondary']};" + f" border-top: 1px solid {REFINED_PALETTE['border']};" + ) + bottom_layout = QVBoxLayout(bottom) + bottom_layout.setContentsMargins(24, 16, 24, 16) + bottom_layout.setSpacing(12) + + self.progress = QProgressBar() + self.progress.setTextVisible(False) + self.progress.setFixedHeight(5) + bottom_layout.addWidget(self.progress) + + btn_layout = QHBoxLayout() + btn_layout.addStretch() + + self.about_btn = QPushButton() + self.about_btn.setProperty("variant", "ghost") + self.about_btn.clicked.connect(self.show_about) + btn_layout.addWidget(self.about_btn) + + btn_layout.addStretch() + bottom_layout.addLayout(btn_layout) + layout.addWidget(bottom) + + def retranslate_ui(self): + """Apply translated strings to every labelled widget.""" + self.setWindowTitle(f"{self.translator.get('app_title')} v{APP_VERSION}") + self.title_label.setText(self.translator.get('app_title')) + self.subtitle_label.setText(self.translator.get("version") + f" {APP_VERSION}") + self.files_label.setText(self.translator.get("files")) + self.add_files_btn.setText(self.translator.get("add_files")) + self.add_folder_btn.setText(self.translator.get("add_folder")) + self.main_action_btn.setText(self.translator.get("start_patching")) + self.clear_btn.setText(self.translator.get("clear_all")) + self.empty_text.setText(self.translator.get("files_not_added")) + self.empty_hint.setText(self.translator.get("add_pe_files_hint")) + self.settings_title.setText(self.translator.get("settings")) + self.log_title.setText(self.translator.get("logs")) + self.clear_log_btn.setText(self.translator.get("clear")) + self.about_btn.setText(self.translator.get("about")) + self.api_group.setTitle(self.translator.get("api_group_title")) + self.all_apis.setText(self.translator.get("all_apis")) + self.options_group.setTitle(self.translator.get("options_group_title")) + self.backup.setText(self.translator.get("create_backups")) + self.overwrite.setText(self.translator.get("overwrite_originals")) + + def log(self, msg_key: str, level="info", args: list = None): + """Append a timestamped, colour-coded message to the log widget.""" + if args is None: + args = [] + formatted_msg = self.translator.get(msg_key, *args) + if not formatted_msg: + self.log_text.append("") + return + colors = { + 'info': REFINED_PALETTE['text_secondary'], + 'success': REFINED_PALETTE['success'], + 'warning': REFINED_PALETTE['warning'], + 'error': REFINED_PALETTE['error'], + } + timestamp = datetime.now().strftime("%H:%M:%S") + color = colors.get(level, colors['info']) + self.log_text.append( + f'{timestamp}' + f' {formatted_msg}' + ) + + def show_task_error(self, title_key, message): + """Show a warning dialog for a task-manager error.""" + _msg(QMessageBox.Icon.Warning, self, self.translator.get(title_key), message) + + def add_files(self): + """Open a file picker and queue the selected PE files.""" + files, _ = QFileDialog.getOpenFileNames( + self, + self.translator.get("dialog_select_pe_files"), + "", + self.translator.get("dialog_pe_files_filter"), + ) + if files: + self.process_files(files) + + def add_folder(self): + """Prompt for sub-folder option, pick a folder, and launch the scan dialog.""" + dlg = FolderSearchDialog(self, self.translator) + dlg.exec() + if dlg.result_choice == FolderSearchDialog.CANCELLED: + return + include_subfolders = dlg.result_choice == FolderSearchDialog.RECURSIVE + folder = QFileDialog.getExistingDirectory( + self, self.translator.get("dialog_select_folder") + ) + if folder: + subfolder_hint = ( + self.translator.get('log_with_subfolders') if include_subfolders else '' + ) + self.log('log_scanning_folder', 'info', [folder, subfolder_hint]) + scan_dialog = RefinedFolderDialog(self, folder, include_subfolders) + accepted = scan_dialog.exec() == QDialog.DialogCode.Accepted + found_files = scan_dialog.get_found_files() + if accepted and found_files: + self.process_files(found_files) + + def process_files(self, paths): + """Start background validation of *paths* and add valid files to the list.""" + existing = {f['path'] for f in self.files} + new = [p for p in paths if p not in existing] + if not new: + self.log("log_all_files_added", "warning") + return + self.progress.setRange(0, 0) + worker = self.thread_manager.start_task( + FileProcessorWorker, + self.translator.get("task_analyzing_files"), + new, + ) + if not worker: + self.progress.setRange(0, 100) + return + worker.file_processed.connect(self.add_file_item) + worker.finished.connect(self.files_added) + + def add_file_item(self, info): + """Add a validated file entry to the list widget.""" + if self.empty_state.isVisible(): + self.empty_state.hide() + self.files_container.show() + item = SwipeableFileItem(info, self.translator) + item.removed.connect(self.remove_file_with_animation) + self.files.append(info) + self.file_items[info['path']] = item + self.files_layout.addWidget(item) + + def files_added(self, added, errors): + """Reset the progress bar and log the outcome of file processing.""" + self.progress.setRange(0, 100) + self.progress.setValue(0) + self.update_stats() + if added: + self.log("log_files_added", "success", [added]) + if errors: + self.log("log_files_skipped", "warning", [errors]) + + def remove_file_with_animation(self, path): + """Trigger the removal animation for the file at *path*.""" + if path in self.file_items: + self.animate_card_removal(path) + + def animate_card_removal(self, path: str): + """Run a slide-out + shrink animation then finalise removal.""" + widget = self.file_items.get(path) + if not widget: + return + anim_group = QParallelAnimationGroup(widget) + slide_anim = QPropertyAnimation(widget, b"pos") + slide_anim.setDuration(300) + slide_anim.setStartValue(widget.pos()) + slide_anim.setEndValue(QPoint(widget.width() * -1, widget.pos().y())) + slide_anim.setEasingCurve(QEasingCurve.Type.InCubic) + shrink_anim = QPropertyAnimation(widget, b"maximumHeight") + shrink_anim.setDuration(250) + shrink_anim.setStartValue(widget.height()) + shrink_anim.setEndValue(0) + anim_group.addAnimation(slide_anim) + anim_group.addAnimation(shrink_anim) + anim_group.finished.connect(lambda: self.finalize_removal(path, widget)) + anim_group.start() + + def finalize_removal(self, path, item): + """Remove the file record and widget after the animation finishes.""" + self.files = [f for f in self.files if f['path'] != path] + self.file_items.pop(path, None) + item.deleteLater() + if not self.files: + self.empty_state.show() + self.files_container.hide() + self.update_stats() + + def clear_all(self): + """Remove all files from the list after confirmation.""" + if self.thread_manager.is_running(): + _msg( + QMessageBox.Icon.Warning, self, + self.translator.get("dialog_op_in_progress"), + self.translator.get("dialog_cannot_clear"), + ) + return + if not self.files: + _msg( + QMessageBox.Icon.Information, self, + self.translator.get("dialog_list_empty"), + self.translator.get("dialog_no_files_to_clear"), + ) + return + if not _msg_question( + self, + self.translator.get("dialog_confirmation"), + self.translator.get("dialog_clear_all_q"), + ): + for item in self.file_items.values(): + item.deleteLater() + self.files.clear() + self.file_items.clear() + self.empty_state.show() + self.files_container.hide() + self.update_stats() + self.log("log_list_cleared", "info") + + def clear_log(self): + """Clear the log widget and log the clear action itself.""" + self.log_text.clear() + self.log("log_cleared", "info") + + def _show_log_context_menu(self, pos): + """Show a dark-themed context menu for the log text widget.""" + # Save the current selection — right-click steals focus and clears it + cursor = self.log_text.textCursor() + selection_start = cursor.selectionStart() + selection_end = cursor.selectionEnd() + + standard_menu = self.log_text.createStandardContextMenu() + menu = DarkMenu(self.log_text) + for action in standard_menu.actions(): + menu.addAction(action) + standard_menu.setParent(None) + menu.exec(self.log_text.mapToGlobal(pos)) + + # Restore the selection after the menu closes + cursor = self.log_text.textCursor() + cursor.setPosition(selection_start) + cursor.setPosition(selection_end, cursor.MoveMode.KeepAnchor) + self.log_text.setTextCursor(cursor) + + def update_stats(self): + """Refresh the file-count badge in the header.""" + self.files_count.setText(str(len(self.files))) + + def toggle_apis(self, state): + """Check or uncheck all individual API checkboxes at once.""" + for check in self.api_checks.values(): + check.setChecked(bool(state)) + + def on_api_change(self): + """Keep the 'All APIs' master checkbox in sync with individual ones.""" + all_checked = all(c.isChecked() for c in self.api_checks.values()) + self.all_apis.blockSignals(True) + self.all_apis.setChecked(all_checked) + self.all_apis.blockSignals(False) + + def on_main_action_click(self): + """Toggle between starting a patch run and requesting cancellation.""" + if self.thread_manager.is_running(): + self.thread_manager.stop_current_task() + self.main_action_btn.setText(self.translator.get("cancelling")) + self.main_action_btn.setEnabled(False) + else: + self.start_patching() + + def set_ui_for_patching(self, is_patching: bool): + """Enable or disable interactive controls during a patch operation.""" + if is_patching: + self.main_action_btn.setText(self.translator.get("cancel")) + self.main_action_btn.setEnabled(True) + self.clear_btn.setEnabled(False) + self.add_files_btn.setEnabled(False) + self.add_folder_btn.setEnabled(False) + for item in self.file_items.values(): + item.setEnabled(False) + else: + self.main_action_btn.setText(self.translator.get("start_patching")) + self.main_action_btn.setEnabled(True) + self.clear_btn.setEnabled(True) + self.add_files_btn.setEnabled(True) + self.add_folder_btn.setEnabled(True) + for item in self.file_items.values(): + item.setEnabled(True) + + def start_patching(self): + """Validate the selection and launch the PatcherWorker thread.""" + if not self.files: + _msg( + QMessageBox.Icon.Warning, self, + self.translator.get("warning_title"), + self.translator.get("warning_no_files"), + ) + return + + apis = [key for key, checkbox in self.api_checks.items() if checkbox.isChecked()] + if self.all_apis.isChecked(): + apis = list(DLL_REPLACEMENTS.keys()) + if not apis: + _msg( + QMessageBox.Icon.Warning, self, + self.translator.get("warning_title"), + self.translator.get("warning_no_api"), + ) + return + + self.set_ui_for_patching(True) + self.progress.setValue(0) + + worker = self.thread_manager.start_task( + PatcherWorker, + self.translator.get("task_patching_files"), + list(self.files), + apis, + self.backup.isChecked(), + self.overwrite.isChecked(), + ) + + if not worker: + self.set_ui_for_patching(False) + return + + worker.log_message.connect(self.log) + worker.progress_updated.connect(self.progress.setValue) + worker.file_status_updated.connect(self.on_file_status_updated) + worker.finished.connect(self.patching_done) + + def on_file_status_updated(self, path: str, status: str, text_key: str): + """Update the visual status of a file row without removing it yet.""" + widget = self.file_items.get(path) + if widget: + widget.update_status(status, self.translator.get(text_key)) + + def patching_done( + self, + stats: Tuple[int, int, int], + was_cancelled: bool, + cancelled_file_index: int = -1, + total_files: int = 0, + remaining_files: int = 0, + ): + """Handle patching completion: clean up the file list and show a summary.""" + s, k, e = stats + + # -1 is used instead of None because Qt Signal cannot carry NoneType int + if cancelled_file_index == -1: + cancelled_file_index = None + + if total_files is None or total_files == 0: + total_files = ( + cancelled_file_index + len(self.files) + if cancelled_file_index else len(self.files) + ) + if remaining_files is None: + remaining_files = ( + total_files - cancelled_file_index + if cancelled_file_index is not None else 0 + ) + + self.progress.setValue(0) + self.set_ui_for_patching(False) + + if was_cancelled and cancelled_file_index is not None: + paths_to_remove = [self.files[i]['path'] for i in range(cancelled_file_index)] + for path in paths_to_remove: + if path in self.file_items: + widget = self.file_items[path] + self.files_layout.removeWidget(widget) + widget.deleteLater() + self.file_items.pop(path, None) + self.files = self.files[cancelled_file_index:] + if not self.files: + self.files_container.hide() + self.empty_state.show() + self.update_stats() + self.log( + "log_patched_files_removed", "info", + [cancelled_file_index, total_files, remaining_files] + ) + else: + if not was_cancelled: + for path in list(self.file_items.keys()): + widget = self.file_items[path] + self.files_layout.removeWidget(widget) + widget.deleteLater() + self.file_items.pop(path, None) + self.files.clear() + self.files_container.hide() + self.empty_state.show() + self.update_stats() + + summary_parts = [] + if s > 0: + summary_parts.append(self.translator.get("summary_patched", s)) + if k > 0: + summary_parts.append(self.translator.get("summary_skipped", k)) + if e > 0: + summary_parts.append(self.translator.get("summary_errors", e)) + + if was_cancelled: + summary_text = ( + self.translator.get("summary_cancelled_prefix") + ", ".join(summary_parts) + ) + else: + summary_text = ( + self.translator.get("summary_finished_prefix") + ", ".join(summary_parts) + if summary_parts else self.translator.get("summary_no_ops") + ) + + level = "success" if e == 0 and s > 0 and not was_cancelled else "warning" + self.log(summary_text, level, []) + + if not was_cancelled: + _msg( + QMessageBox.Icon.Information, self, + self.translator.get("dialog_completed_title"), + summary_text, + ) + else: + self.log("log_operation_stopped", "info") + + def show_cancel_dialog(): + """Show a dialog reporting how many files were not processed.""" + if remaining_files > 0: + cancel_message = self.translator.get( + "dialog_cancel_remaining", remaining_files + ) + _msg( + QMessageBox.Icon.Information, self, + self.translator.get("dialog_cancelled_title"), + cancel_message, + ) + + QTimer.singleShot(500, show_cancel_dialog) + + def show_about(self): + """Open the About dialog.""" + about_dialog = AboutDialog(self, self.translator) + about_dialog.exec() + + +# ============================================================================= +# ENTRY POINT +# ============================================================================= +if __name__ == '__main__': + app = QApplication(sys.argv) + app.setStyle('Fusion') + + # Explicitly set a font to avoid system font OpenType warnings (e.g. "Adwaita Sans" on Linux) + _preferred_fonts = ["Inter", "Segoe UI", "DejaVu Sans", "Liberation Sans", "Noto Sans"] + _app_font = QFont() + for _fname in _preferred_fonts: + if QFont(_fname).exactMatch(): + _app_font.setFamily(_fname) + break + _app_font.setPointSize(10) + app.setFont(_app_font) + + app.setStyleSheet(REFINED_STYLESHEET) + + lang_code, show_dialog = load_settings() + + if show_dialog: + dialog = LanguageDialog() + if dialog.exec() == QDialog.DialogCode.Accepted: + lang_code, show_dialog_next_time = dialog.get_selection() + save_settings(lang_code, show_dialog_next_time) + else: + sys.exit(0) + + translator = TranslationManager(lang_code) + + qt_translator = QTranslator() + if lang_code != "en": + translations_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath) + if qt_translator.load(f"qt_{lang_code}.qm", translations_path): + app.installTranslator(qt_translator) + + window = PEPatcherGUI(translator) + window.show() + sys.exit(app.exec()) diff --git a/API_PE_Replacer/requirements.txt b/API_PE_Replacer/requirements.txt index 14f1458..f15c0ab 100644 --- a/API_PE_Replacer/requirements.txt +++ b/API_PE_Replacer/requirements.txt @@ -1,2 +1,3 @@ -PyQt6 -pefile \ No newline at end of file +PySide6 +lief +defusedxml \ No newline at end of file diff --git a/API_PE_Replacer/run.bat b/API_PE_Replacer/run.bat index 6c3b3ac..a1d70d4 100644 --- a/API_PE_Replacer/run.bat +++ b/API_PE_Replacer/run.bat @@ -1,6 +1,19 @@ @echo off setlocal +REM --- ADMIN CHECK --- +echo Checking for administrator privileges... +whoami /groups | find "S-1-16-12288" >nul 2>&1 +if %errorlevel% neq 0 ( + echo - Not running as administrator. Restarting with elevated privileges... + powershell -Command "Start-Process '%~f0' -Verb RunAs" + exit /b +) +echo + Running as administrator. + +REM Change to the directory where the script is located +cd /d "%~dp0" + REM --- CONFIGURATION --- REM Name of the virtual environment directory set VENV_DIR=venv @@ -35,13 +48,17 @@ echo + Environment activated. REM Install dependencies from requirements.txt echo [3/4] Installing/checking dependencies... -pip install -r %REQUIREMENTS_FILE% -if %errorlevel% neq 0 ( - echo [ERROR] Failed to install dependencies from %REQUIREMENTS_FILE%. - pause - exit /b 1 +if not exist "%REQUIREMENTS_FILE%" ( + echo - %REQUIREMENTS_FILE% not found. Skipping dependency installation. +) else ( + pip install -r %REQUIREMENTS_FILE% + if %errorlevel% neq 0 ( + echo [ERROR] Failed to install dependencies from %REQUIREMENTS_FILE%. + pause + exit /b 1 + ) + echo + Dependencies are up to date. ) -echo + Dependencies are up to date. REM Run the main script echo [4/4] Starting the application... @@ -50,4 +67,5 @@ python %MAIN_SCRIPT% echo. echo Application finished. +pause endlocal \ No newline at end of file diff --git a/API_PE_Replacer/run.sh b/API_PE_Replacer/run.sh new file mode 100644 index 0000000..8a57ac9 --- /dev/null +++ b/API_PE_Replacer/run.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -e + +# Change to the directory where the script is located +cd "$(dirname "$0")" + +# --- CONFIGURATION --- +VENV_DIR="venv" +REQUIREMENTS_FILE="requirements.txt" +MAIN_SCRIPT="main.py" + +# --- SCRIPT LOGIC --- +echo "[1/4] Checking for virtual environment..." + +if [ ! -d "$VENV_DIR" ]; then + echo " - Directory '$VENV_DIR' not found. Creating a new environment..." + python3 -m venv "$VENV_DIR" || { echo "[ERROR] Failed to create virtual environment. Is Python 3 installed?"; exit 1; } + echo " + Virtual environment created successfully." +else + echo " + Virtual environment found." +fi + +echo "[2/4] Activating environment..." +source "$VENV_DIR/bin/activate" +echo " + Environment activated." + +echo "[3/4] Installing/checking dependencies..." +if [ ! -f "$REQUIREMENTS_FILE" ]; then + echo " - $REQUIREMENTS_FILE not found. Skipping dependency installation." +else + pip install -r "$REQUIREMENTS_FILE" || { echo "[ERROR] Failed to install dependencies from $REQUIREMENTS_FILE."; exit 1; } + echo " + Dependencies are up to date." +fi + +echo "[4/4] Starting the application..." +echo "" +python "$MAIN_SCRIPT" + +echo "" +echo "Application finished." \ No newline at end of file