From 5d440ee17a8d27128a8630e807b5cbd1f479f120 Mon Sep 17 00:00:00 2001 From: EXLOUD Date: Thu, 5 Mar 2026 19:16:20 +0200 Subject: [PATCH 1/4] Upgrade to v1.0.12 --- API_PE_Replacer/CHANGELOG.md | 201 ++ API_PE_Replacer/config.py | 32 +- API_PE_Replacer/main.py | 3963 ++++++++++++++++++------------ API_PE_Replacer/requirements.txt | 5 +- API_PE_Replacer/run.bat | 30 +- API_PE_Replacer/run.sh | 40 + 6 files changed, 2630 insertions(+), 1641 deletions(-) create mode 100644 API_PE_Replacer/CHANGELOG.md create mode 100644 API_PE_Replacer/run.sh diff --git a/API_PE_Replacer/CHANGELOG.md b/API_PE_Replacer/CHANGELOG.md new file mode 100644 index 0000000..fcdcc20 --- /dev/null +++ b/API_PE_Replacer/CHANGELOG.md @@ -0,0 +1,201 @@ +# Changelog + +--- + +## [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. 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..c6c2338 100644 --- a/API_PE_Replacer/main.py +++ b/API_PE_Replacer/main.py @@ -1,1619 +1,2344 @@ -# -*- 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, +) +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 # 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.12" +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']}; }} +""" + + +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 + + 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: + 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'} + try: + binary = lief.PE.parse(path) + if binary is not None: + chars = binary.header.characteristics + if chars & 0x2000: + info['type'] = 'DLL' + elif chars & 0x0002: + info['type'] = 'EXE' + else: + info['type'] = 'PE' + try: + info['arch'] = ( + 'x64' + if binary.header.machine == lief.PE.MACHINE_TYPES.AMD64 + else 'x86' + ) + except Exception: # pylint: disable=broad-exception-caught + info['arch'] = 'x64' if int(binary.header.machine) == 0x8664 else 'x86' + except Exception as exc: # pylint: disable=broad-exception-caught + print(f"⚠️ Warning: could not read PE metadata: {exc}") + 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) + 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.setFixedSize(600, 690) + 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, 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 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.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.""" + QMessageBox.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.""" + 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: + 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.""" + 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): + """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) + self.update_stats() + + 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) + 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(): + 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 + question = QMessageBox.question( + self, + self.translator.get("dialog_confirmation"), + self.translator.get("dialog_clear_all_q"), + ) + if question == 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): + """Clear the log widget and log the clear action itself.""" + self.log_text.clear() + self.log("log_cleared", "info") + + 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: + 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): + """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: + QMessageBox.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 + ) + QMessageBox.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') + 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 From 1ccece821100c64a678e1ae61197c9a34a6ecd65 Mon Sep 17 00:00:00 2001 From: EXLOUD Date: Sat, 7 Mar 2026 22:44:56 +0200 Subject: [PATCH 2/4] Upgrade to v1.0.13 --- API_PE_Replacer/CHANGELOG.md | 73 +++++++- API_PE_Replacer/main.py | 333 ++++++++++++++++++++++++++++------- 2 files changed, 344 insertions(+), 62 deletions(-) diff --git a/API_PE_Replacer/CHANGELOG.md b/API_PE_Replacer/CHANGELOG.md index fcdcc20..dfe68dc 100644 --- a/API_PE_Replacer/CHANGELOG.md +++ b/API_PE_Replacer/CHANGELOG.md @@ -2,6 +2,77 @@ --- +## [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 @@ -198,4 +269,4 @@ _Initial release._ - Backup and overwrite options. - Ukrainian interface. - Supported APIs: WINHTTP, WININET, WS2_32, SENSAPI, IPHLPAPI, URLMON, NETAPI32, - WSOCK32, WINTRUST. + WSOCK32, WINTRUST. \ No newline at end of file diff --git a/API_PE_Replacer/main.py b/API_PE_Replacer/main.py index c6c2338..e0c3636 100644 --- a/API_PE_Replacer/main.py +++ b/API_PE_Replacer/main.py @@ -39,13 +39,13 @@ QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTextEdit, QFrame, QFileDialog, QMessageBox, QCheckBox, QDialog, QProgressBar, QScrollArea, QGraphicsDropShadowEffect, - QGroupBox, QSplitter, QTextBrowser, + 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 # pylint: disable=no-name-in-module +from PySide6.QtGui import QColor, QFont, QPalette # pylint: disable=no-name-in-module try: from config import DLL_REPLACEMENTS @@ -160,10 +160,143 @@ def get_base_path(): 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']}; }} + 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() @@ -493,37 +626,60 @@ def __init__(self, file_paths): 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: - with open(path, 'rb') as f: - if f.read(2) != b'MZ': + 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', 'arch': 'x86'} - try: - binary = lief.PE.parse(path) - if binary is not None: - chars = binary.header.characteristics - if chars & 0x2000: - info['type'] = 'DLL' - elif chars & 0x0002: - info['type'] = 'EXE' - else: - info['type'] = 'PE' - try: - info['arch'] = ( - 'x64' - if binary.header.machine == lief.PE.MACHINE_TYPES.AMD64 - else 'x86' - ) - except Exception: # pylint: disable=broad-exception-caught - info['arch'] = 'x64' if int(binary.header.machine) == 0x8664 else 'x86' - except Exception as exc: # pylint: disable=broad-exception-caught - print(f"⚠️ Warning: could not read PE metadata: {exc}") + 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 @@ -899,6 +1055,26 @@ def __init__(self, parent=None): 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): @@ -1480,7 +1656,11 @@ def __init__(self, parent, translator): super().__init__(parent) self.translator = translator self.setWindowTitle(self.translator.get("about")) - self.setFixedSize(600, 690) + self.setMinimumSize(600, 690) + self.setWindowFlags( + self.windowFlags() + | Qt.WindowType.WindowMaximizeButtonHint + ) self.setup_ui() def setup_ui(self): @@ -1508,7 +1688,7 @@ def setup_ui(self): donations_panel = QWidget() donations_panel_layout = QVBoxLayout(donations_panel) - donations_panel_layout.setContentsMargins(0, 0, 0, 0) + donations_panel_layout.setContentsMargins(0, 16, 0, 0) donations_panel_layout.setSpacing(8) title_label = QLabel(self.translator.get("donation_title")) @@ -1875,6 +2055,17 @@ def _create_log_panel(self): 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;" @@ -1958,7 +2149,7 @@ def log(self, msg_key: str, level="info", args: list = None): def show_task_error(self, title_key, message): """Show a warning dialog for a task-manager error.""" - QMessageBox.warning(self, self.translator.get(title_key), message) + _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.""" @@ -1973,22 +2164,11 @@ def add_files(self): def add_folder(self): """Prompt for sub-folder option, pick a folder, and launch the scan dialog.""" - 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: + dlg = FolderSearchDialog(self, self.translator) + dlg.exec() + if dlg.result_choice == FolderSearchDialog.CANCELLED: return - include_subfolders = msg_box.clickedButton() == yes_button + include_subfolders = dlg.result_choice == FolderSearchDialog.RECURSIVE folder = QFileDialog.getExistingDirectory( self, self.translator.get("dialog_select_folder") ) @@ -2005,7 +2185,8 @@ def add_folder(self): def process_files(self, paths): """Start background validation of *paths* and add valid files to the list.""" - new = [p for p in paths if not any(f['path'] == p for f in self.files)] + 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 @@ -2031,12 +2212,12 @@ def add_file_item(self, info): self.files.append(info) self.file_items[info['path']] = item self.files_layout.addWidget(item) - self.update_stats() 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: @@ -2080,25 +2261,24 @@ def finalize_removal(self, path, item): def clear_all(self): """Remove all files from the list after confirmation.""" if self.thread_manager.is_running(): - QMessageBox.warning( - self, + _msg( + QMessageBox.Icon.Warning, self, self.translator.get("dialog_op_in_progress"), self.translator.get("dialog_cannot_clear"), ) return if not self.files: - QMessageBox.information( - self, + _msg( + QMessageBox.Icon.Information, self, self.translator.get("dialog_list_empty"), self.translator.get("dialog_no_files_to_clear"), ) return - question = QMessageBox.question( + if not _msg_question( self, self.translator.get("dialog_confirmation"), self.translator.get("dialog_clear_all_q"), - ) - if question == QMessageBox.StandardButton.Yes: + ): for item in self.file_items.values(): item.deleteLater() self.files.clear() @@ -2113,6 +2293,26 @@ def clear_log(self): 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))) @@ -2160,8 +2360,8 @@ def set_ui_for_patching(self, is_patching: bool): def start_patching(self): """Validate the selection and launch the PatcherWorker thread.""" if not self.files: - QMessageBox.warning( - self, + _msg( + QMessageBox.Icon.Warning, self, self.translator.get("warning_title"), self.translator.get("warning_no_files"), ) @@ -2171,8 +2371,8 @@ def start_patching(self): if self.all_apis.isChecked(): apis = list(DLL_REPLACEMENTS.keys()) if not apis: - QMessageBox.warning( - self, + _msg( + QMessageBox.Icon.Warning, self, self.translator.get("warning_title"), self.translator.get("warning_no_api"), ) @@ -2285,8 +2485,8 @@ def patching_done( self.log(summary_text, level, []) if not was_cancelled: - QMessageBox.information( - self, + _msg( + QMessageBox.Icon.Information, self, self.translator.get("dialog_completed_title"), summary_text, ) @@ -2299,8 +2499,8 @@ def show_cancel_dialog(): cancel_message = self.translator.get( "dialog_cancel_remaining", remaining_files ) - QMessageBox.information( - self, + _msg( + QMessageBox.Icon.Information, self, self.translator.get("dialog_cancelled_title"), cancel_message, ) @@ -2319,6 +2519,17 @@ def show_about(self): 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() From 7d10a8b63bb66dd472c24631fa34f0e05da119ea Mon Sep 17 00:00:00 2001 From: EXLOUD Date: Sat, 7 Mar 2026 22:46:58 +0200 Subject: [PATCH 3/4] Fixed program version numbering --- API_PE_Replacer/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API_PE_Replacer/main.py b/API_PE_Replacer/main.py index e0c3636..2dd4ce0 100644 --- a/API_PE_Replacer/main.py +++ b/API_PE_Replacer/main.py @@ -69,7 +69,7 @@ 'github': 'https://github.com/EXLOUD', } -APP_VERSION = "1.0.12" +APP_VERSION = "1.0.13" LANG_FOLDER = "languages" From cf98b1f60f2742a4f2d426b45ce1314c9fb31c02 Mon Sep 17 00:00:00 2001 From: EXLOUD Date: Sat, 18 Apr 2026 21:47:10 +0300 Subject: [PATCH 4/4] New library implementations --- API_NEW/exmsw/dllmain.c | 267 ++++++++--------------------- API_NEW/exmsw/ucrt/arm64/EXMSW.dll | Bin 0 -> 15360 bytes API_NEW/exmsw/ucrt/x32/EXMSW.dll | Bin 0 -> 13824 bytes API_NEW/exmsw/ucrt/x64/EXMSW.dll | Bin 0 -> 14848 bytes API_NEW/exmsw/version.rc | 4 +- 5 files changed, 74 insertions(+), 197 deletions(-) create mode 100644 API_NEW/exmsw/ucrt/arm64/EXMSW.dll create mode 100644 API_NEW/exmsw/ucrt/x32/EXMSW.dll create mode 100644 API_NEW/exmsw/ucrt/x64/EXMSW.dll 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 0000000000000000000000000000000000000000..a05d688734bc13416fe70f7c5de048d6c7cb7ee9 GIT binary patch literal 15360 zcmeHO4RBP)k?yx|C9MP)0Rm*Otwms4$OsS!3=UXtB_sFMd|?&+B~Z(09%6G=p51(+YjT)9EH3G`cXUb=_*h&>bzZV9{V?H+d^5RBS4srGPV!0rp!*F5aB*9Ys=h4Y9i%ZTa{sZ{@q zUY{YppK|RJEVmG4gJRdHwYLLjigJwg6lDuhsvr$x+6#g{2@BPNRqJG+v2~4datYCI z(}>#dBl-uH86xUpIknJH49G-0F-H_D2c}oZ5D>>+)+1C%T$ms24w+m1DGBKLod|W=t&;45&&BXejwvfh&mUBG)6JU zTC^}9Y+-&BmKTnMJs=93giv6^7XeINjW9|135ME;@_7iKa|rF^DyR~ z1D5!i-4s8!)DoAdBP?5DRVh-e>K!>JF(6Z7NTy^=EqNg&tzWl1 zEit;UxgX-Wn&oSok*&&)p~&q(yh(-SP?heJa%#XNSEC)ozw za!&rsqiFj#Qj+OMK_74)gI%Py%R4M>d6>FDx~63`v6*NIk=a!xNrTckve zS3vije@AV0;0v&W*Fw!a9(8{^Z2Nq9@*-?+rUZ!W?I zNLW*Oo-*d(X(~_FQjS*sq*i8IbNj&LoGXKAlw|&1#Q3X#U8HmrE7ahTsG4nnG}={S zx5aH8nwX09Ucu+tGi7j&AfTG3nTgzO196gQ2IPpE19_X~++dmxI_dXamld+Xg3ScHM2E z_Bud|F?aZ!5josae0n+bIemt6CFn|O z{A8+FDrlW9VL0SS4m!c;t8zT$FWbOi8X8ZtylN&LFnSL zQo~lP3$}9)a*U=|8f-Q#0loIIZlZil)4{yvhIVs4liDsK=l1ndd;oh4=dFv#H@#nw z+ur`Fwe6inlTM z45521Vm%LW1ARI;emM>qV5fw>9-lYIW-;bv#~yXiO3U&%ZgyGX>DOE>^HZoH{aQv# zVrXdDD<6~JI0znYCkbm8W5=-mc+R#4ZN8kFYY~67SWALlrqOpIF4#Zw(LNut^B_AT zg{I_Ro6zztUG_<$Wui~L)+bMlqv>z9qn+us+Fp+M6Hg``K2-?%;?Ge>CdE!wmaKRq z^OlldX2NHgsVF~P{mYyTdcXhP{a?wnxWCJM-BQl!uAy(A%6$C6l^=?HY)IoHvC{D! zId*Ca9sPZdWy)6M#_GyFN^*)t=_vff?5UG0TB;_0^QG{RG-c~S*dTK%#VhxSTq103 z*xZnaqx0U(&nX>yiIz)O&z$Ur@7eyWpM2GIawl2NyCFxqnt5_B?7=yl+aDer%KQ;@ zGy3Bwyqsb^SGcVUKDMAQ_vsRSG@0KkPpzY}0PEm0?Vx`(Gcn*UN1eu}>JbVw)JUv7a=&2frCIFY0;U zkcY7!;j!=j>5>Ufc1C@Z}r}74dqGL3Xi?8oHrh@kDB<1-7H#MA%>uve*{y0poqniC57q zpkn^y&&KTFfh>QI1 zO`{Gajf%{?$Dp&(7Qwz|ZhLibxb2lx*d#-F#(}Y!N3-{g#P`9o-=fVw23f3E9%9eT zGhV#CVxfkw&nkW9xGvAo{mUkO1|%x_sPmLzlWDNcE0{CK!L=PH#*N{zajV4z_L}_t zm^1G;y!SZZj|{}!8SE!G$HLCj6D_*GN5`?|cgWeH`CG(ru9D13rN?rltnvsMw?hDK^N;Tf}W6JZz(4|$P$86GPzv9iM0hQEyb(zf=ws=lG2{s|Jry^_*gJST5!5A5q*t#o|ic;0jA ziI%Dg-hZX^t#d%HC{&tr3TzF>u{Teo2|uZruQc1FjOT1_im#odG|x{@e{RC0^ygaP zha73bdF0RPj7dtzenp;_0w@{e3tG5p35Z6Md$>+bc#KnekZl1r;GUQw}IEFi~T+~ zW#x6RDxWr$e{CwiWh$RBl@nT7^U2M>|6eWe{->57`RAYf ze99MpOfgr*!RLQJs@^}UKH3gA!~Cy2N+n_Y9Q$~eX!MYd$8e0-X$;d}kIT1*W$dZM1e!R1Zr;Hw>_2i@)vIBe+pa=!YLt9a=q%g|c zK&>McWqzD`t4dLDl!3}TVU+I#7twwAR|s5)kAL^lLVPOJfNA{vIpf)Jnl2|d`SEWP zFl&YPbMdsy)BSeqNWW&?HL|X=|3#>O36TT|*i!|3fOhA%#|3~RNZ>pw`GF`(>x_&WW0}P${Yx6`vHeep$5Ulepz)OH10p0@i11!sl zrU2}K1%MTRhXGFm>H(hxd=qdKa02ia;J1KFfV6vwW&#QUYXM%svw#-?-v_)2xCEHG zf+!cT3{Vbu0?+{13rGN71^fr#5+HLW(IUV)z$XBo0elJ226zeZbHLjGE51J80k{XS z44?oW1k?aF0fK-ipc(Kipat+e;H!Xd0yu^&B$Jg=@Xe#afx#~-9b56VRI;#=F&W@w7ckTtd04&roV?4PyyE1BCIrC zb4&3teHpG2@1+&Ak`%1QkC6kP$yd>8x}S=vlpG$98j6-RQCXm|zA_w)20cN4#4)m3 zLl3A?gVP!GY<1Vwg(HH9f*OgGG;eU%tBx@g(UF@G}+a5xxVj}hEkR6%AjrqPD0HMHJ`ABIHL8ebrSzNNvyX5W@ZP<+9_A8NzgH&m_* zy6admXUOeQBecO;Y2;1V1e6HRT8(Z9Mtz%o9w9SAPBnU6`{8m8iLNz~x{%%qLNH{m zS9?OfP?4*~S>e#^R~>Xl!@j^48g3ck4-*}pC<#Mbk2?}ITrsj48gKK}so@72eRU&S z41TuS*cLUqHn2GuuGcI!tO-WMzG1`$^Nj3UU+UBE(y3Gak#x~cQ zMU0zl9(D&J^}cAi&#w}uTcvuoVKvqaFTT2fOKl3d19h}TjRw@{rse<^DOV!!Z7>ui zAE++C_F#A`g+29k6owS>XLC4Mk3y5`p)iuvwoot}rHCt}a!#R$8ubPvQ6UdyLmo2u z$2dD{k3~V|#&4`zO|;3N^@DcBM4JVgvKqS`=*DvoXtki_nP?Az76MH%(fpvbfHvOK zrPJTi@dX`c+^=!Z((wWvm+SaR9s70std3vQajTAvF~6(VU(<0=Yl!_?uj8JCAAC~Q zFgA34_+TUObRBDm=$zhfP{(%^8!Z~n8&xmY>s6!ZPwMr}I*#f11s%Vn8N|qh^V})Q+!9#p! zOR=5vz*4U}OuMX)G^*j|N;NFDwtz=14K@a%YMA;`s&UZq zi{BN#%VnM#PESZgBNDv>=c&=O8l0m*)6yxbp_l=Z`)n&@Rn^j^%C-@>S>Isu3&(fT~YTY zB-^C~eGHQtUDUufUpN@3hd1#qF@j@6XPhTRZy2#2C(G+)mx>dmE24T`n|*#Xy7;~F zp|YwCW$Tt|FPSyYB`&?Vv9#*Z4b^McmlYN*7B5WhkguRVQn1|@DDZ@%1+behSl}-# zC@i=^JD->S00Vv%97XV$IgD&@V_Z#2_5?Ja>N^VIRzB`$xQ?AfQ1r<%^4EV`ZRS*EtxD4$+)(vpgX0u%iq zs?09I8NFxsFRUk@z3VFXdl7@;n+vtyil5`o0-lEoo)=L5x~ZN)ij8&LgWn9Wm-iQ*LpCY+sReAY zD_7VdAN9GWI`$viS@7b!hQN3+6?h6|3Va|Njd(DQsB5*GfB&y7Fr+^pbzI}S?MLnZ zv#0E*HorbJ#QjBlepEp>`+Xz}@GaP3MLQ_I3d}c1Zvyiz)_cHwBjux9iEpL2&NoX} zfcaL;<{-KNoCVx6lgJKSiFF++A>#Gz1?~ra40ti}V=Zt9I0oDVya4>I$Z=8bhrH&a zPC5$wChGkEJ|3YH(qZ5?aE|G`gK#6#Ntwt)6Vbm9cm;5Uo$n|e(q7<4(cTBV7WKt* z(1!Lmfj^BCPi-#Y{-u*zfXjfpfQ7udyl-^U3Sh1`>Gh+)DwRlfl1u&55k;EFU9EWZJmveO zL_grp`csP&lwUGx+*X9jM%-h&@VAVrP+Ch5(+1Sn;%_<1{2a6XZcxi0+&LAX;bLyY zW5qwyFoJd)2omH)aUae%>02Pphr4SPk^{6E_vDCmh>>ttR|Hro@Fx7_d*!L1mx8Ar z|7FMx{$^<7Moks6w?I-97HPyy_(I%-Gv6X{o4yVjKPv8}AH)r!bbZV<=otYg>k>j= zANt@BiHGmhx1!aq-~2lvp_Cp%eGRR{KYkYDj5{oABe?MbZIV%_-|H_I*x=`RMg?yG zWB5%os7LSZ0PWiX);;b#C5(kvd%?*w4`I%%?H0@v2U2{0;Mw{?@d({`&TiCr77Ff7 zCFrg3GmXbH?9DaJawErGh;J19HG#iVOhSL{3r8Ks@nA%=y>DbAbcOHOHY59vw$Nz% ztU*5s5$A@+hL*!o!EX)-VEo)L3=aMV;|DhTEZusT8E92;Q@!85O$|qI$+cyHNg9_4;8tcr#kBjV2RPZYZ}m%E{31g|7vU$j}P zf)G|48Zm@gR~hzg!)xCbHDa_e|MliF@iK$Cu2Z+Ee!HLluD;tHSsU0E+^UA}wm16t znmQ6$efMU!Kce2f$Plw?(RCrC$5^$3KYki@^ZBzafMmwG{)GdF b4;(!ZI@om3dnj}$cIeNRbF;(Wqy_#PB_@^? literal 0 HcmV?d00001 diff --git a/API_NEW/exmsw/ucrt/x32/EXMSW.dll b/API_NEW/exmsw/ucrt/x32/EXMSW.dll new file mode 100644 index 0000000000000000000000000000000000000000..0e2154689e7c79f0d0bbec5c27be18889a2cb7cc GIT binary patch literal 13824 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 literal 0 HcmV?d00001 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