Toolbox for analyzing and editing pkg application files for psp,ps3, ps4 and ps5, includes the most useful functions you might need.
| 1 | import logging |
| 2 | import sys |
| 3 | import os |
| 4 | import re |
| 5 | from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, |
| 6 | QHBoxLayout, QLabel, QLineEdit, QPushButton, QTabWidget, |
| 7 | QMessageBox, QToolBar, QAction, QTreeWidget, QTextEdit, QTableWidget, QFileDialog, QGroupBox, QGridLayout, QSpinBox, QTreeWidgetItem, QDialog, QProgressBar, QShortcut, QActionGroup, QComboBox, QCheckBox, QListWidget, QFrame) |
| 8 | from PyQt5.QtCore import Qt, QSize, QUrl, QObject, pyqtSignal, QThread |
| 9 | from PyQt5.QtGui import QFont, QDesktopServices |
| 10 | from PyQt5.QtWidgets import QStyle |
| 11 | import struct |
| 12 | from GraphicUserInterface.components import FileBrowser, WallpaperViewer |
| 13 | from GraphicUserInterface.dialogs import SettingsDialog |
| 14 | from GraphicUserInterface.utils import StyleManager, ImageUtils, FileUtils |
| 15 | from GraphicUserInterface.widgets import ExtractTab, InfoTab, BruteforceTab |
| 16 | from GraphicUserInterface.widgets.pfs_info_tab import PfsInfoTab |
| 17 | from tools.PS5_Game_Info import PS5GameInfo |
| 18 | from packages import PackagePS4, PackagePS5, PackagePS3 |
| 19 | from file_operations import extract_file, inject_file, modify_file_header |
| 20 | from Utilities.Trophy import ESMFDecrypter, TRPCreator |
| 21 | from tools.PS4_Passcode_Bruteforcer import PS4PasscodeBruteforcer |
| 22 | import re |
| 23 | from Utilities import Logger |
| 24 | import json |
| 25 | from PyQt5.QtWidgets import QTableWidgetItem |
| 26 | from PyQt5.QtGui import QKeySequence |
| 27 | import traceback |
| 28 | from Utilities import Logger, SettingsManager, TRPReader |
| 29 | from .locales.translator import Translator |
| 30 | from GraphicUserInterface.utils.update_checker import UpdateChecker, UpdateDialog |
| 31 | |
| 32 | class MainWindow(QMainWindow): |
| 33 | COLORS = { |
| 34 | 'light': { |
| 35 | 'background': '#f5f6fa', |
| 36 | 'text': '#2f3640', |
| 37 | 'accent': '#3498db', |
| 38 | 'secondary': '#e1e5eb', |
| 39 | 'success': '#2ecc71', |
| 40 | 'warning': '#f1c40f', |
| 41 | 'error': '#e74c3c', |
| 42 | 'tree_alternate': '#f1f2f6', |
| 43 | 'tree_hover': '#dcdde1', |
| 44 | 'tree_selected': '#3498db' |
| 45 | }, |
| 46 | 'dark': { |
| 47 | 'background': '#2f3640', |
| 48 | 'text': '#f5f6fa', |
| 49 | 'accent': '#3498db', |
| 50 | 'secondary': '#353b48', |
| 51 | 'success': '#27ae60', |
| 52 | 'warning': '#f39c12', |
| 53 | 'error': '#c0392b', |
| 54 | 'tree_alternate': '#353b48', |
| 55 | 'tree_hover': '#485460', |
| 56 | 'tree_selected': '#3498db' |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | def __init__(self, temp_directory): |
| 61 | super().__init__() |
| 62 | self.temp_directory = temp_directory |
| 63 | self.package = None |
| 64 | |
| 65 | # Initialize settings manager |
| 66 | self.settings = SettingsManager() |
| 67 | |
| 68 | # Initialize translator |
| 69 | self.translator = Translator() |
| 70 | |
| 71 | # Load and apply appearance settings |
| 72 | self.settings_dict = StyleManager.load_settings() |
| 73 | appearance = self.settings_dict.get("appearance", {}) |
| 74 | # Font |
| 75 | self.font = QFont( |
| 76 | appearance.get("font_family", "Arial"), |
| 77 | appearance.get("font_size", 12) |
| 78 | ) |
| 79 | QApplication.setFont(self.font) |
| 80 | # Theme |
| 81 | StyleManager.apply_theme(self, self.settings_dict) |
| 82 | |
| 83 | # Setup UI |
| 84 | self.setup_ui() |
| 85 | self.setup_settings_button() |
| 86 | |
| 87 | # Apply saved language after UI is built (menus/tabs exist) |
| 88 | try: |
| 89 | self._apply_saved_language() |
| 90 | except Exception: |
| 91 | pass |
| 92 | |
| 93 | # Enable drag and drop |
| 94 | self.setAcceptDrops(True) |
| 95 | |
| 96 | self.setup_shortcuts() |
| 97 | self.setup_drag_drop() |
| 98 | |
| 99 | # Initialize update checker |
| 100 | self.update_checker = UpdateChecker(self) |
| 101 | self.update_checker.update_available.connect(self.show_update_dialog) |
| 102 | self.update_checker.error_occurred.connect(self.handle_update_error) |
| 103 | |
| 104 | # Check for updates |
| 105 | if not self.should_skip_updates(): |
| 106 | self.update_checker.start() |
| 107 | |
| 108 | def _apply_saved_language(self): |
| 109 | """Read saved language from settings and apply to translator, then refresh UI.""" |
| 110 | saved = self.settings_dict.get("language", "English") |
| 111 | # Accept either display names or language codes |
| 112 | name_to_code = { |
| 113 | 'English': 'en', 'Italian': 'it', 'Spanish': 'es', |
| 114 | 'French': 'fr', 'German': 'de', 'Japanese': 'ja' |
| 115 | } |
| 116 | code = saved.lower() if len(saved) in (2, 3) else name_to_code.get(saved, 'en') |
| 117 | if hasattr(self, 'translator'): |
| 118 | if self.translator.change_language(code): |
| 119 | if hasattr(self, 'retranslate_ui'): |
| 120 | self.retranslate_ui() |
| 121 | |
| 122 | def set_style(self): |
| 123 | """Modern UI styling using Qt-supported properties only""" |
| 124 | self.setStyleSheet(""" |
| 125 | /* Main Window */ |
| 126 | QMainWindow { |
| 127 | background: qlineargradient(x1:0, y1:0, x2:1, y2:1, |
| 128 | stop:0 rgba(74, 144, 226, 25%), |
| 129 | stop:0.3 rgba(80, 227, 194, 15%), |
| 130 | stop:0.7 rgba(245, 101, 101, 15%), |
| 131 | stop:1 rgba(196, 113, 237, 25%)); |
| 132 | } |
| 133 | |
| 134 | /* Cards */ |
| 135 | QWidget { |
| 136 | background: rgba(255, 255, 255, 20%); |
| 137 | border: 1px solid rgba(255, 255, 255, 30%); |
| 138 | border-radius: 16px; |
| 139 | } |
| 140 | |
| 141 | /* Modern Input Fields */ |
| 142 | QLineEdit, QTextEdit, QPlainTextEdit { |
| 143 | background: rgba(255, 255, 255, 18%); |
| 144 | border: 2px solid transparent; |
| 145 | border-radius: 12px; |
| 146 | padding: 14px 18px; |
| 147 | font-size: 14px; |
| 148 | font-weight: 500; |
| 149 | color: #2d3748; |
| 150 | selection-background-color: rgba(74, 144, 226, 30%); |
| 151 | } |
| 152 | QLineEdit:focus, QTextEdit:focus, QPlainTextEdit:focus { |
| 153 | border: 2px solid rgba(74, 144, 226, 60%); |
| 154 | background: rgba(255, 255, 255, 24%); |
| 155 | } |
| 156 | |
| 157 | /* Revolutionary Buttons */ |
| 158 | QPushButton { |
| 159 | background: qlineargradient(x1:0, y1:0, x2:1, y2:1, |
| 160 | stop:0 rgba(74, 144, 226, 90%), |
| 161 | stop:1 rgba(80, 227, 194, 90%)); |
| 162 | border: none; |
| 163 | border-radius: 14px; |
| 164 | padding: 12px 24px; |
| 165 | font-size: 14px; |
| 166 | font-weight: 600; |
| 167 | color: white; |
| 168 | } |
| 169 | QPushButton:hover { |
| 170 | background: qlineargradient(x1:0, y1:0, x2:1, y2:1, |
| 171 | stop:0 rgba(74, 144, 226, 100%), |
| 172 | stop:1 rgba(80, 227, 194, 100%)); |
| 173 | } |
| 174 | QPushButton:pressed { |
| 175 | background: qlineargradient(x1:0, y1:0, x2:1, y2:1, |
| 176 | stop:0 rgba(74, 144, 226, 85%), |
| 177 | stop:1 rgba(80, 227, 194, 85%)); |
| 178 | } |
| 179 | QPushButton:disabled { |
| 180 | background: rgba(160, 174, 192, 0.4); |
| 181 | color: rgba(160, 174, 192, 0.8); |
| 182 | } |
| 183 | |
| 184 | /* Floating Tree/List Widgets */ |
| 185 | QTreeWidget, QListWidget { |
| 186 | background: rgba(255, 255, 255, 0.1); |
| 187 | border: 1px solid rgba(255, 255, 255, 0.15); |
| 188 | border-radius: 16px; |
| 189 | padding: 8px; |
| 190 | alternate-background-color: rgba(255, 255, 255, 0.05); |
| 191 | color: #2d3748; |
| 192 | font-weight: 500; |
| 193 | } |
| 194 | QTreeWidget::item, QListWidget::item { |
| 195 | padding: 8px 12px; |
| 196 | border-radius: 8px; |
| 197 | margin: 2px 0px; |
| 198 | } |
| 199 | QTreeWidget::item:hover, QListWidget::item:hover { |
| 200 | background: rgba(74, 144, 226, 0.15); |
| 201 | color: #1a202c; |
| 202 | } |
| 203 | QTreeWidget::item:selected, QListWidget::item:selected { |
| 204 | background: qlineargradient(x1:0, y1:0, x2:1, y2:0, |
| 205 | stop:0 rgba(74, 144, 226, 0.8), |
| 206 | stop:1 rgba(80, 227, 194, 0.8)); |
| 207 | color: white; |
| 208 | font-weight: 600; |
| 209 | } |
| 210 | |
| 211 | /* Modern Headers */ |
| 212 | QHeaderView::section { |
| 213 | background: rgba(255, 255, 255, 0.12); |
| 214 | border: none; |
| 215 | border-radius: 8px; |
| 216 | padding: 12px; |
| 217 | font-weight: 600; |
| 218 | color: #4a5568; |
| 219 | margin: 2px; |
| 220 | } |
| 221 | |
| 222 | /* Invisible Tabs (handled by sidebar) */ |
| 223 | QTabWidget::pane { |
| 224 | background: transparent; |
| 225 | border: none; |
| 226 | } |
| 227 | QTabBar::tab { |
| 228 | background: transparent; |
| 229 | border: none; |
| 230 | padding: 0px; |
| 231 | margin: 0px; |
| 232 | } |
| 233 | |
| 234 | /* Floating Labels */ |
| 235 | QLabel { |
| 236 | background: transparent; |
| 237 | color: #2d3748; |
| 238 | font-weight: 500; |
| 239 | border: none; |
| 240 | } |
| 241 | |
| 242 | /* Glass Menu Bar */ |
| 243 | QMenuBar { |
| 244 | background: rgba(255, 255, 255, 12%); |
| 245 | border: none; |
| 246 | border-radius: 12px; |
| 247 | padding: 4px 8px; |
| 248 | color: #2d3748; |
| 249 | font-weight: 500; |
| 250 | } |
| 251 | QMenuBar::item { |
| 252 | background: transparent; |
| 253 | padding: 8px 16px; |
| 254 | border-radius: 8px; |
| 255 | } |
| 256 | QMenuBar::item:selected { |
| 257 | background: rgba(74, 144, 226, 0.15); |
| 258 | } |
| 259 | QMenu { |
| 260 | background: rgba(255, 255, 255, 95%); |
| 261 | border: 1px solid rgba(255, 255, 255, 30%); |
| 262 | border-radius: 12px; |
| 263 | padding: 8px; |
| 264 | } |
| 265 | QMenu::item { |
| 266 | padding: 10px 20px; |
| 267 | border-radius: 8px; |
| 268 | color: #2d3748; |
| 269 | } |
| 270 | QMenu::item:selected { |
| 271 | background: rgba(74, 144, 226, 0.15); |
| 272 | } |
| 273 | |
| 274 | /* Modern Combo Boxes */ |
| 275 | QComboBox { |
| 276 | background: rgba(255, 255, 255, 0.12); |
| 277 | border: 2px solid rgba(255, 255, 255, 0.2); |
| 278 | border-radius: 12px; |
| 279 | padding: 10px 16px; |
| 280 | font-weight: 500; |
| 281 | color: #2d3748; |
| 282 | } |
| 283 | QComboBox:hover { |
| 284 | background: rgba(255, 255, 255, 0.18); |
| 285 | border-color: rgba(74, 144, 226, 0.4); |
| 286 | } |
| 287 | QComboBox::drop-down { |
| 288 | border: none; |
| 289 | width: 30px; |
| 290 | } |
| 291 | QComboBox QAbstractItemView { |
| 292 | background: rgba(255, 255, 255, 95%); |
| 293 | border: 1px solid rgba(255, 255, 255, 30%); |
| 294 | border-radius: 12px; |
| 295 | selection-background-color: rgba(74, 144, 226, 20%); |
| 296 | } |
| 297 | |
| 298 | /* Elegant Scroll Bars */ |
| 299 | QScrollBar:vertical { |
| 300 | background: rgba(255, 255, 255, 0.1); |
| 301 | width: 8px; |
| 302 | border-radius: 4px; |
| 303 | margin: 0px; |
| 304 | } |
| 305 | QScrollBar::handle:vertical { |
| 306 | background: rgba(74, 144, 226, 0.6); |
| 307 | border-radius: 4px; |
| 308 | min-height: 20px; |
| 309 | } |
| 310 | QScrollBar::handle:vertical:hover { |
| 311 | background: rgba(74, 144, 226, 0.8); |
| 312 | } |
| 313 | QScrollBar:horizontal { |
| 314 | background: rgba(255, 255, 255, 0.1); |
| 315 | height: 8px; |
| 316 | border-radius: 4px; |
| 317 | margin: 0px; |
| 318 | } |
| 319 | QScrollBar::handle:horizontal { |
| 320 | background: rgba(74, 144, 226, 0.6); |
| 321 | border-radius: 4px; |
| 322 | min-width: 20px; |
| 323 | } |
| 324 | |
| 325 | /* Floating Tooltips */ |
| 326 | QToolTip { |
| 327 | background: rgba(45, 55, 72, 95%); |
| 328 | color: white; |
| 329 | border: 1px solid rgba(255, 255, 255, 20%); |
| 330 | border-radius: 8px; |
| 331 | padding: 8px 12px; |
| 332 | font-weight: 500; |
| 333 | } |
| 334 | |
| 335 | /* Modern Status Bar */ |
| 336 | QStatusBar { |
| 337 | background: rgba(255, 255, 255, 0.08); |
| 338 | border: none; |
| 339 | border-radius: 12px; |
| 340 | color: #4a5568; |
| 341 | font-weight: 500; |
| 342 | } |
| 343 | |
| 344 | /* Glass Group Boxes */ |
| 345 | QGroupBox { |
| 346 | background: rgba(255, 255, 255, 12%); |
| 347 | border: 1px solid rgba(255, 255, 255, 20%); |
| 348 | border-radius: 16px; |
| 349 | margin-top: 12px; |
| 350 | padding-top: 12px; |
| 351 | font-weight: 600; |
| 352 | color: #2d3748; |
| 353 | } |
| 354 | QGroupBox::title { |
| 355 | subcontrol-origin: margin; |
| 356 | subcontrol-position: top left; |
| 357 | padding: 4px 12px; |
| 358 | background: rgba(74, 144, 226, 10%); |
| 359 | border-radius: 8px; |
| 360 | color: #2d3748; |
| 361 | } |
| 362 | """) |
| 363 | |
| 364 | def setup_ui(self): |
| 365 | """Setup the main UI""" |
| 366 | self.setWindowTitle("PKG Tool Box v1.4.0") |
| 367 | self.setGeometry(100, 100, 1200, 800) |
| 368 | |
| 369 | # Central widget |
| 370 | central_widget = QWidget() |
| 371 | self.setCentralWidget(central_widget) |
| 372 | main_layout = QVBoxLayout(central_widget) |
| 373 | |
| 374 | # Split layout |
| 375 | split_layout = QHBoxLayout() |
| 376 | |
| 377 | # Left panel for PKG info |
| 378 | left_panel = QWidget() |
| 379 | left_layout = QVBoxLayout(left_panel) |
| 380 | |
| 381 | # PKG icon and info - Revolutionary glass card |
| 382 | self.image_label = QLabel() |
| 383 | self.image_label.setFixedSize(320, 320) |
| 384 | self.image_label.setAlignment(Qt.AlignCenter) |
| 385 | self.image_label.setStyleSheet(""" |
| 386 | QLabel { |
| 387 | background: rgba(255, 255, 255, 15%); |
| 388 | border: 2px solid rgba(74, 144, 226, 30%); |
| 389 | border-radius: 24px; |
| 390 | padding: 20px; |
| 391 | } |
| 392 | """) |
| 393 | |
| 394 | self.content_id_label = QLabel() |
| 395 | self.content_id_label.setStyleSheet(""" |
| 396 | QLabel { |
| 397 | font-size: 16px; |
| 398 | font-weight: 600; |
| 399 | color: #2d3748; |
| 400 | padding: 16px 20px; |
| 401 | background: rgba(255, 255, 255, 12%); |
| 402 | border: 1px solid rgba(255, 255, 255, 20%); |
| 403 | border-radius: 16px; |
| 404 | margin: 8px 0px; |
| 405 | } |
| 406 | """) |
| 407 | |
| 408 | left_layout.addWidget(self.image_label) |
| 409 | left_layout.addWidget(self.content_id_label) |
| 410 | |
| 411 | # Revolutionary drag-drop zone with glassmorphism |
| 412 | self.drag_drop_label = QLabel("✨ Drop PKG files here or Browse") |
| 413 | self.drag_drop_label.setAlignment(Qt.AlignCenter) |
| 414 | self.drag_drop_label.setStyleSheet(""" |
| 415 | QLabel { |
| 416 | font-size: 20px; |
| 417 | font-weight: 600; |
| 418 | color: #4a5568; |
| 419 | padding: 40px; |
| 420 | border: 3px dashed rgba(74, 144, 226, 40%); |
| 421 | border-radius: 24px; |
| 422 | background: qlineargradient(x1:0, y1:0, x2:1, y2:1, |
| 423 | stop:0 rgba(74, 144, 226, 8%), |
| 424 | stop:0.5 rgba(80, 227, 194, 6%), |
| 425 | stop:1 rgba(196, 113, 237, 8%)); |
| 426 | } |
| 427 | """) |
| 428 | left_layout.addWidget(self.drag_drop_label) |
| 429 | |
| 430 | # Revolutionary file selection with glassmorphism |
| 431 | pkg_layout = QHBoxLayout() |
| 432 | self.pkg_entry = QLineEdit() |
| 433 | self.pkg_entry.setPlaceholderText("🎯 Select your PKG file...") |
| 434 | self.pkg_entry.setStyleSheet(""" |
| 435 | QLineEdit { |
| 436 | padding: 16px 20px; |
| 437 | border: 2px solid rgba(74, 144, 226, 20%); |
| 438 | border-radius: 16px; |
| 439 | font-size: 15px; |
| 440 | font-weight: 500; |
| 441 | background: rgba(255, 255, 255, 12%); |
| 442 | color: #2d3748; |
| 443 | } |
| 444 | QLineEdit:focus { |
| 445 | border-color: rgba(74, 144, 226, 60%); |
| 446 | background: rgba(255, 255, 255, 18%); |
| 447 | } |
| 448 | QLineEdit:hover { |
| 449 | background: rgba(255, 255, 255, 15%); |
| 450 | } |
| 451 | """) |
| 452 | |
| 453 | browse_button = QPushButton("🚀 BROWSE") |
| 454 | browse_button.clicked.connect(self.browse_pkg) |
| 455 | browse_button.setStyleSheet(""" |
| 456 | QPushButton { |
| 457 | background: qlineargradient(x1:0, y1:0, x2:1, y2:1, |
| 458 | stop:0 rgba(74, 144, 226, 90%), |
| 459 | stop:1 rgba(80, 227, 194, 90%)); |
| 460 | color: white; |
| 461 | font-weight: 700; |
| 462 | padding: 16px 28px; |
| 463 | border: none; |
| 464 | border-radius: 16px; |
| 465 | font-size: 15px; |
| 466 | } |
| 467 | QPushButton:hover { |
| 468 | background: qlineargradient(x1:0, y1:0, x2:1, y2:1, |
| 469 | stop:0 rgba(74, 144, 226, 100%), |
| 470 | stop:1 rgba(80, 227, 194, 100%)); |
| 471 | } |
| 472 | QPushButton:pressed { |
| 473 | background: qlineargradient(x1:0, y1:0, x2:1, y2:1, |
| 474 | stop:0 rgba(74, 144, 226, 85%), |
| 475 | stop:1 rgba(80, 227, 194, 85%)); |
| 476 | } |
| 477 | """) |
| 478 | |
| 479 | pkg_layout.addWidget(self.pkg_entry) |
| 480 | pkg_layout.addWidget(browse_button) |
| 481 | left_layout.addLayout(pkg_layout) |
| 482 | |
| 483 | left_layout.addStretch() |
| 484 | |
| 485 | # Revolutionary tab widget with glassmorphism |
| 486 | self.tab_widget = QTabWidget() |
| 487 | self.tab_widget.setStyleSheet(""" |
| 488 | QTabWidget::pane { |
| 489 | background: rgba(255, 255, 255, 8%); |
| 490 | border: 1px solid rgba(255, 255, 255, 15%); |
| 491 | border-radius: 24px; |
| 492 | padding: 20px; |
| 493 | } |
| 494 | QTabBar::tab { |
| 495 | background: transparent; |
| 496 | border: none; |
| 497 | padding: 0px; |
| 498 | margin: 0px; |
| 499 | } |
| 500 | """) |
| 501 | |
| 502 | # Info tab |
| 503 | self.info_tab = InfoTab(self) |
| 504 | self.tab_widget.addTab(self.info_tab, "Info") |
| 505 | |
| 506 | # File Browser tab |
| 507 | |
| 508 | self.file_browser = FileBrowser(self) |
| 509 | self.tab_widget.addTab(self.file_browser, "File Browser") |
| 510 | |
| 511 | # Wallpaper tab |
| 512 | self.wallpaper_viewer = WallpaperViewer(self) |
| 513 | self.tab_widget.addTab(self.wallpaper_viewer, "Wallpaper") |
| 514 | |
| 515 | # Extract tab |
| 516 | self.extract_tab = ExtractTab(self) |
| 517 | self.tab_widget.addTab(self.extract_tab, "Extract") |
| 518 | |
| 519 | # PFS Info tab |
| 520 | self.pfs_info_tab = PfsInfoTab(self) |
| 521 | self.tab_widget.addTab(self.pfs_info_tab, "PFS Info") |
| 522 | |
| 523 | # Inject tab |
| 524 | self.inject_tab = QWidget() |
| 525 | self.setup_inject_tab() |
| 526 | self.tab_widget.addTab(self.inject_tab, "Inject") |
| 527 | |
| 528 | # Modify tab |
| 529 | self.modify_tab = QWidget() |
| 530 | self.setup_modify_tab() |
| 531 | self.tab_widget.addTab(self.modify_tab, "Modify") |
| 532 | |
| 533 | # Trophy tab |
| 534 | self.trophy_tab = QWidget() |
| 535 | self.setup_trophy_tab() |
| 536 | self.tab_widget.addTab(self.trophy_tab, "Trophy") |
| 537 | |
| 538 | # ESMF Decrypter tab |
| 539 | self.esmf_decrypter_tab = QWidget() |
| 540 | self.setup_esmf_decrypter_tab() |
| 541 | self.tab_widget.addTab(self.esmf_decrypter_tab, "ESMF Decrypter") |
| 542 | |
| 543 | # Create TRP tab |
| 544 | self.trp_create_tab = QWidget() |
| 545 | self.setup_trp_create_tab() |
| 546 | self.tab_widget.addTab(self.trp_create_tab, "Create TRP") |
| 547 | |
| 548 | # PS5 Game Info tab |
| 549 | self.ps5_game_info_tab = QWidget() |
| 550 | self.setup_ps5_game_info_tab() |
| 551 | self.tab_widget.addTab(self.ps5_game_info_tab, "PS5 Game Info") |
| 552 | |
| 553 | # Passcode Bruteforcer tab |
| 554 | self.bruteforce_tab = BruteforceTab(self) |
| 555 | self.tab_widget.addTab(self.bruteforce_tab, "Passcode Bruteforcer") |
| 556 | |
| 557 | # Hide native tab bar – navigation handled by sidebar and build sidebar |
| 558 | self.tab_widget.tabBar().hide() |
| 559 | self.create_sidebar() |
| 560 | |
| 561 | split_layout.addWidget(self.sidebar_frame) |
| 562 | split_layout.addWidget(left_panel, 1) |
| 563 | split_layout.addWidget(self.tab_widget, 2) |
| 564 | main_layout.addLayout(split_layout) |
| 565 | |
| 566 | # Credits and social buttons |
| 567 | credits_layout = QHBoxLayout() |
| 568 | |
| 569 | # Left side - Credits label |
| 570 | credits_label = QLabel() |
| 571 | credits_label.setText('<a href="https://github.com/seregonwar" style="text-decoration:none; color:#2d3748;">Created by <b>SeregonWar</b></a>') |
| 572 | credits_label.setTextFormat(Qt.RichText) |
| 573 | credits_label.setOpenExternalLinks(True) |
| 574 | credits_label.setStyleSheet(""" |
| 575 | QLabel { |
| 576 | font-size: 14px; |
| 577 | font-weight: 600; |
| 578 | color: #2d3748; |
| 579 | padding: 6px 8px; |
| 580 | background: rgba(255, 255, 255, 12%); |
| 581 | border: 1px solid rgba(0,0,0,8%); |
| 582 | border-radius: 8px; |
| 583 | } |
| 584 | """) |
| 585 | credits_layout.addWidget(credits_label, 0, Qt.AlignLeft) |
| 586 | |
| 587 | # Center - Social buttons |
| 588 | social_layout = QHBoxLayout() |
| 589 | social_layout.setSpacing(12) |
| 590 | |
| 591 | # Stile comune per i pulsanti social (pill buttons) |
| 592 | social_button_style = """ |
| 593 | QPushButton { |
| 594 | font-size: 12px; |
| 595 | color: white; |
| 596 | background-color: #3498db; |
| 597 | border: none; |
| 598 | border-radius: 14px; |
| 599 | padding: 6px 14px; |
| 600 | min-width: 88px; |
| 601 | height: 28px; |
| 602 | font-weight: 600; |
| 603 | } |
| 604 | QPushButton:hover { |
| 605 | background-color: #2980b9; |
| 606 | } |
| 607 | """ |
| 608 | |
| 609 | x_button = QPushButton("X") |
| 610 | x_button.setToolTip("Open X / Twitter") |
| 611 | github_button = QPushButton("GitHub") |
| 612 | github_button.setToolTip("Open GitHub profile") |
| 613 | reddit_button = QPushButton("Reddit") |
| 614 | reddit_button.setToolTip("Open Reddit profile") |
| 615 | |
| 616 | for button in [x_button, github_button, reddit_button]: |
| 617 | button.setStyleSheet(social_button_style) |
| 618 | social_layout.addWidget(button) |
| 619 | |
| 620 | # Connetti i pulsanti agli URL |
| 621 | x_button.clicked.connect(lambda: QDesktopServices.openUrl(QUrl("https://x.com/SeregonWar"))) |
| 622 | github_button.clicked.connect(lambda: QDesktopServices.openUrl(QUrl("https://github.com/seregonwar"))) |
| 623 | reddit_button.clicked.connect(lambda: QDesktopServices.openUrl(QUrl("https://www.reddit.com/user/S3R3GON/"))) |
| 624 | |
| 625 | social_widget = QWidget() |
| 626 | social_widget.setLayout(social_layout) |
| 627 | credits_layout.addWidget(social_widget, 1, Qt.AlignCenter) |
| 628 | |
| 629 | # Right side - Ko-fi button |
| 630 | kofi_button = QPushButton("Support on Ko-fi") |
| 631 | kofi_button.setToolTip("Buy me a coffee on Ko-fi") |
| 632 | kofi_button.setStyleSheet(""" |
| 633 | QPushButton { |
| 634 | font-size: 12px; |
| 635 | color: white; |
| 636 | background-color: #e74c3c; |
| 637 | border: none; |
| 638 | border-radius: 14px; |
| 639 | padding: 6px 14px; |
| 640 | min-width: 140px; |
| 641 | height: 28px; |
| 642 | font-weight: 700; |
| 643 | } |
| 644 | QPushButton:hover { |
| 645 | background-color: #c0392b; |
| 646 | } |
| 647 | """) |
| 648 | kofi_button.clicked.connect(lambda: QDesktopServices.openUrl(QUrl("https://ko-fi.com/seregon"))) |
| 649 | credits_layout.addWidget(kofi_button, 0, Qt.AlignRight) |
| 650 | |
| 651 | # Aggiungi il layout dei credits al layout principale |
| 652 | main_layout.addLayout(credits_layout) |
| 653 | |
| 654 | # Aggiungi menu bar |
| 655 | menubar = self.menuBar() |
| 656 | |
| 657 | # Store menu references |
| 658 | self.file_menu = menubar.addMenu('File') |
| 659 | self.tools_menu = menubar.addMenu('Tools') |
| 660 | self.view_menu = menubar.addMenu('View') |
| 661 | self.help_menu = menubar.addMenu('Help') |
| 662 | self.links_menu = menubar.addMenu('Links') |
| 663 | |
| 664 | # File menu actions |
| 665 | self.open_action = QAction('Open PKG', self) |
| 666 | self.open_action.setShortcut('Ctrl+O') |
| 667 | self.open_action.triggered.connect(self.browse_pkg) |
| 668 | self.file_menu.addAction(self.open_action) |
| 669 | |
| 670 | self.file_menu.addSeparator() |
| 671 | |
| 672 | self.exit_action = QAction('Exit', self) |
| 673 | self.exit_action.setShortcut('Ctrl+Q') |
| 674 | self.exit_action.triggered.connect(self.close) |
| 675 | self.file_menu.addAction(self.exit_action) |
| 676 | |
| 677 | # Tools menu actions |
| 678 | extract_action = QAction('Extract PKG', self) |
| 679 | extract_action.triggered.connect(lambda: self.tab_widget.setCurrentWidget(self.extract_tab)) |
| 680 | self.tools_menu.addAction(extract_action) |
| 681 | |
| 682 | inject_action = QAction('Inject File', self) |
| 683 | inject_action.triggered.connect(lambda: self.tab_widget.setCurrentWidget(self.inject_tab)) |
| 684 | self.tools_menu.addAction(inject_action) |
| 685 | |
| 686 | modify_action = QAction('Modify PKG', self) |
| 687 | modify_action.triggered.connect(lambda: self.tab_widget.setCurrentWidget(self.modify_tab)) |
| 688 | self.tools_menu.addAction(modify_action) |
| 689 | |
| 690 | self.tools_menu.addSeparator() |
| 691 | |
| 692 | trophy_action = QAction('Trophy Tools', self) |
| 693 | trophy_action.triggered.connect(lambda: self.tab_widget.setCurrentWidget(self.trophy_tab)) |
| 694 | self.tools_menu.addAction(trophy_action) |
| 695 | |
| 696 | esmf_action = QAction('ESMF Decrypter', self) |
| 697 | esmf_action.triggered.connect(lambda: self.tab_widget.setCurrentWidget(self.esmf_decrypter_tab)) |
| 698 | self.tools_menu.addAction(esmf_action) |
| 699 | |
| 700 | trp_action = QAction('Create TRP', self) |
| 701 | trp_action.triggered.connect(lambda: self.tab_widget.setCurrentWidget(self.trp_create_tab)) |
| 702 | self.tools_menu.addAction(trp_action) |
| 703 | |
| 704 | self.tools_menu.addSeparator() |
| 705 | |
| 706 | bruteforce_action = QAction('Passcode Bruteforcer', self) |
| 707 | bruteforce_action.triggered.connect(lambda: self.tab_widget.setCurrentWidget(self.bruteforce_tab)) |
| 708 | self.tools_menu.addAction(bruteforce_action) |
| 709 | |
| 710 | # View menu |
| 711 | view_menu = self.view_menu |
| 712 | |
| 713 | file_browser_action = QAction('File Browser', self) |
| 714 | file_browser_action.triggered.connect(lambda: self.tab_widget.setCurrentWidget(self.file_browser)) |
| 715 | view_menu.addAction(file_browser_action) |
| 716 | |
| 717 | wallpaper_action = QAction('Wallpaper Viewer', self) |
| 718 | wallpaper_action.triggered.connect(lambda: self.tab_widget.setCurrentWidget(self.wallpaper_viewer)) |
| 719 | view_menu.addAction(wallpaper_action) |
| 720 | |
| 721 | # Links menu |
| 722 | links_menu = self.links_menu |
| 723 | |
| 724 | github_action = QAction('GitHub', self) |
| 725 | github_action.triggered.connect(lambda: QDesktopServices.openUrl(QUrl("https://github.com/seregonwar"))) |
| 726 | links_menu.addAction(github_action) |
| 727 | |
| 728 | reddit_action = QAction('Reddit', self) |
| 729 | reddit_action.triggered.connect(lambda: QDesktopServices.openUrl(QUrl("https://www.reddit.com/user/S3R3GON/"))) |
| 730 | links_menu.addAction(reddit_action) |
| 731 | |
| 732 | x_action = QAction('X (Twitter)', self) |
| 733 | x_action.triggered.connect(lambda: QDesktopServices.openUrl(QUrl("https://x.com/SeregonWar"))) |
| 734 | links_menu.addAction(x_action) |
| 735 | |
| 736 | links_menu.addSeparator() |
| 737 | |
| 738 | kofi_action = QAction('Support on Ko-fi', self) |
| 739 | kofi_action.triggered.connect(lambda: QDesktopServices.openUrl(QUrl("https://ko-fi.com/seregon"))) |
| 740 | links_menu.addAction(kofi_action) |
| 741 | |
| 742 | # Help menu |
| 743 | help_menu = self.help_menu |
| 744 | |
| 745 | about_action = QAction('About', self) |
| 746 | about_action.triggered.connect(self.show_about) |
| 747 | help_menu.addAction(about_action) |
| 748 | |
| 749 | # Theme submenu and actions |
| 750 | theme_menu = view_menu.addMenu('Theme') |
| 751 | theme_group = QActionGroup(self) |
| 752 | self.theme_actions = {} |
| 753 | themes = { |
| 754 | 'Light': {'bg': '#ffffff', 'text': '#000000', 'accent': '#3498db'}, |
| 755 | 'Dark': {'bg': '#2f3640', 'text': '#f5f6fa', 'accent': '#3498db'}, |
| 756 | 'Nord': {'bg': '#2e3440', 'text': '#eceff4', 'accent': '#88c0d0'}, |
| 757 | 'Solarized': {'bg': '#fdf6e3', 'text': '#657b83', 'accent': '#268bd2'} |
| 758 | } |
| 759 | for theme_name, colors in themes.items(): |
| 760 | action = QAction(theme_name, self) |
| 761 | action.setCheckable(True) |
| 762 | action.triggered.connect(lambda checked, t=theme_name, c=colors: self.change_theme(t, c)) |
| 763 | theme_group.addAction(action) |
| 764 | theme_menu.addAction(action) |
| 765 | self.theme_actions[theme_name] = action |
| 766 | # Mark saved theme as checked |
| 767 | saved_theme = self.settings_dict.get("appearance", {}).get("theme", "Light") |
| 768 | if saved_theme in self.theme_actions: |
| 769 | self.theme_actions[saved_theme].setChecked(True) |
| 770 | |
| 771 | # Status bar |
| 772 | self.status_bar = self.statusBar() |
| 773 | self.pkg_info_label = QLabel() |
| 774 | self.status_bar.addPermanentWidget(self.pkg_info_label) |
| 775 | |
| 776 | # Progress bar nella status bar |
| 777 | self.progress_bar = QProgressBar() |
| 778 | self.progress_bar.setMaximumWidth(200) |
| 779 | self.progress_bar.hide() |
| 780 | self.status_bar.addPermanentWidget(self.progress_bar) |
| 781 | |
| 782 | def change_theme(self, theme_name, colors): |
| 783 | """Change and persist theme selection""" |
| 784 | try: |
| 785 | # Map provided colors to StyleManager schema |
| 786 | new_settings = self.settings_dict or {} |
| 787 | if "appearance" not in new_settings: |
| 788 | new_settings["appearance"] = {} |
| 789 | if "colors" not in new_settings["appearance"]: |
| 790 | new_settings["appearance"]["colors"] = {} |
| 791 | new_settings["appearance"]["theme"] = theme_name |
| 792 | new_settings["appearance"]["colors"].update({ |
| 793 | "background": colors.get('bg', '#ffffff'), |
| 794 | "text": colors.get('text', '#000000'), |
| 795 | "accent": colors.get('accent', '#3498db') |
| 796 | }) |
| 797 | # Save and apply |
| 798 | StyleManager.save_settings(new_settings) |
| 799 | self.settings_dict = new_settings |
| 800 | StyleManager.apply_theme(self, self.settings_dict) |
| 801 | # Reflect selection in menu |
| 802 | if hasattr(self, 'theme_actions') and theme_name in self.theme_actions: |
| 803 | self.theme_actions[theme_name].setChecked(True) |
| 804 | except Exception as e: |
| 805 | logging.error(f"Failed to change theme: {e}") |
| 806 | |
| 807 | def create_sidebar(self): |
| 808 | """Create sidebar with navigation; theme control moved to top toolbar""" |
| 809 | self.sidebar_frame = QFrame() |
| 810 | self.sidebar_frame.setObjectName("sidebar") |
| 811 | self.sidebar_frame.setStyleSheet(""" |
| 812 | QFrame#sidebar { |
| 813 | background: rgba(255, 255, 255, 12%); |
| 814 | border: 1px solid rgba(255, 255, 255, 20%); |
| 815 | border-radius: 24px; |
| 816 | } |
| 817 | QPushButton#navBtn { |
| 818 | text-align: left; |
| 819 | padding: 10px 14px; |
| 820 | border: none; |
| 821 | border-radius: 12px; |
| 822 | font-weight: 500; |
| 823 | color: #2d3748; |
| 824 | background: transparent; |
| 825 | font-size: 14px; |
| 826 | } |
| 827 | QPushButton#navBtn:hover { |
| 828 | background: rgba(74, 144, 226, 15%); |
| 829 | color: #1a202c; |
| 830 | } |
| 831 | QPushButton#navBtn:pressed { |
| 832 | background: rgba(74, 144, 226, 25%); |
| 833 | } |
| 834 | """) |
| 835 | layout = QVBoxLayout(self.sidebar_frame) |
| 836 | layout.setContentsMargins(8, 8, 8, 8) |
| 837 | layout.setSpacing(6) |
| 838 | |
| 839 | # Hamburger toggle |
| 840 | self.sidebar_expanded = True |
| 841 | self.sidebar_width_expanded = 260 |
| 842 | self.sidebar_width_collapsed = 52 |
| 843 | self.sidebar_frame.setFixedWidth(self.sidebar_width_expanded) |
| 844 | |
| 845 | toggle_btn = QPushButton("☰") |
| 846 | toggle_btn.setToolTip("Toggle menu") |
| 847 | toggle_btn.setFixedHeight(40) |
| 848 | toggle_btn.setStyleSheet(""" |
| 849 | QPushButton { |
| 850 | background: qlineargradient(x1:0, y1:0, x2:1, y2:1, |
| 851 | stop:0 rgba(74, 144, 226, 20%), |
| 852 | stop:1 rgba(80, 227, 194, 20%)); |
| 853 | border: 1px solid rgba(74, 144, 226, 30%); |
| 854 | border-radius: 12px; |
| 855 | font-size: 18px; |
| 856 | font-weight: bold; |
| 857 | color: #2d3748; |
| 858 | } |
| 859 | QPushButton:hover { |
| 860 | background: qlineargradient(x1:0, y1:0, x2:1, y2:1, |
| 861 | stop:0 rgba(74, 144, 226, 30%), |
| 862 | stop:1 rgba(80, 227, 194, 30%)); |
| 863 | } |
| 864 | """) |
| 865 | toggle_btn.clicked.connect(self.toggle_sidebar) |
| 866 | layout.addWidget(toggle_btn) |
| 867 | |
| 868 | # Navigation buttons |
| 869 | def add_nav(text, widget): |
| 870 | btn = QPushButton(text) |
| 871 | btn.setObjectName("navBtn") |
| 872 | btn.setFixedHeight(36) |
| 873 | btn.setMinimumWidth(220) |
| 874 | btn.clicked.connect(lambda: self.tab_widget.setCurrentWidget(widget)) |
| 875 | layout.addWidget(btn) |
| 876 | return btn |
| 877 | |
| 878 | # Ensure widgets exist before wiring |
| 879 | add_nav("🏷️ Info", self.info_tab) |
| 880 | add_nav("📁 File Browser", self.file_browser) |
| 881 | add_nav("🖼️ Wallpaper", self.wallpaper_viewer) |
| 882 | add_nav("📦 Extract", self.extract_tab) |
| 883 | add_nav("🧩 PFS Info", self.pfs_info_tab) |
| 884 | add_nav("📥 Inject", self.inject_tab) |
| 885 | add_nav("🛠️ Modify", self.modify_tab) |
| 886 | add_nav("🏆 Trophy", self.trophy_tab) |
| 887 | add_nav("🔓 ESMF Decrypter", self.esmf_decrypter_tab) |
| 888 | add_nav("📦 Create TRP", self.trp_create_tab) |
| 889 | add_nav("🕹️ PS5 Game Info", self.ps5_game_info_tab) |
| 890 | add_nav("🔢 Passcode Bruteforcer", self.bruteforce_tab) |
| 891 | |
| 892 | layout.addStretch(1) |
| 893 | |
| 894 | def toggle_sidebar(self): |
| 895 | """Toggle sidebar width between expanded and collapsed states""" |
| 896 | self.sidebar_expanded = not getattr(self, 'sidebar_expanded', True) |
| 897 | new_w = self.sidebar_width_expanded if self.sidebar_expanded else self.sidebar_width_collapsed |
| 898 | self.sidebar_frame.setFixedWidth(new_w) |
| 899 | |
| 900 | def setup_settings_button(self): |
| 901 | """Add top-left 'Tema' button and settings button to toolbar""" |
| 902 | # Theme toolbar (left-most) |
| 903 | theme_toolbar = QToolBar() |
| 904 | theme_toolbar.setIconSize(QSize(24, 24)) |
| 905 | theme_toolbar.setStyleSheet(""" |
| 906 | QToolBar { spacing: 10px; border: none; background: transparent; } |
| 907 | QToolButton { border: none; border-radius: 6px; padding: 6px 10px; font-weight: 600; } |
| 908 | QToolButton:hover { background-color: rgba(52, 152, 219, 20%); } |
| 909 | """) |
| 910 | theme_action = QAction("Tema", self) |
| 911 | theme_action.setToolTip("Cambia tema") |
| 912 | theme_action.triggered.connect(self.show_theme_menu) |
| 913 | theme_toolbar.addAction(theme_action) |
| 914 | self.addToolBar(Qt.TopToolBarArea, theme_toolbar) |
| 915 | theme_toolbar.setMovable(False) |
| 916 | |
| 917 | # Settings toolbar (kept, to the right) |
| 918 | settings_toolbar = QToolBar() |
| 919 | settings_toolbar.setIconSize(QSize(24, 24)) |
| 920 | settings_icon = self.style().standardIcon(QStyle.SP_FileDialogDetailedView) |
| 921 | settings_button = QAction(settings_icon, "", self) |
| 922 | settings_button.setToolTip("Settings") |
| 923 | settings_button.triggered.connect(self.show_settings_dialog) |
| 924 | settings_toolbar.setStyleSheet(""" |
| 925 | QToolBar { spacing: 10px; border: none; background: transparent; } |
| 926 | QToolButton { border: none; border-radius: 6px; padding: 6px; } |
| 927 | QToolButton:hover { background-color: rgba(52, 152, 219, 20%); } |
| 928 | """) |
| 929 | settings_toolbar.addAction(settings_button) |
| 930 | self.addToolBar(Qt.TopToolBarArea, settings_toolbar) |
| 931 | settings_toolbar.setMovable(False) |
| 932 | |
| 933 | def show_theme_menu(self): |
| 934 | """Show a theme selection menu and apply chosen theme""" |
| 935 | from PyQt5.QtWidgets import QMenu |
| 936 | menu = QMenu(self) |
| 937 | themes = { |
| 938 | 'Light': {'bg': '#ffffff', 'text': '#000000', 'accent': '#3498db'}, |
| 939 | 'Dark': {'bg': '#2f3640', 'text': '#f5f6fa', 'accent': '#3498db'}, |
| 940 | 'Nord': {'bg': '#2e3440', 'text': '#eceff4', 'accent': '#88c0d0'}, |
| 941 | 'Solarized': {'bg': '#fdf6e3', 'text': '#657b83', 'accent': '#268bd2'} |
| 942 | } |
| 943 | for name, colors in themes.items(): |
| 944 | act = menu.addAction(name) |
| 945 | act.triggered.connect(lambda checked, n=name, c=colors: self.change_theme(n, c)) |
| 946 | # Position menu under the mouse or near top-left |
| 947 | menu.exec_(self.mapToGlobal(self.rect().topLeft() + self.menuBar().pos())) |
| 948 | |
| 949 | def show_settings_dialog(self): |
| 950 | """Show settings dialog""" |
| 951 | dialog = SettingsDialog(self) |
| 952 | dialog.exec_() |
| 953 | |
| 954 | def dragEnterEvent(self, event): |
| 955 | """Handle drag enter event""" |
| 956 | if event.mimeData().hasUrls(): |
| 957 | for url in event.mimeData().urls(): |
| 958 | if url.toLocalFile().lower().endswith('.pkg'): |
| 959 | event.acceptProposedAction() |
| 960 | self.drag_drop_label.setStyleSheet(""" |
| 961 | QLabel { |
| 962 | font-size: 18px; |
| 963 | color: #27ae60; |
| 964 | padding: 30px; |
| 965 | border: 3px dashed #27ae60; |
| 966 | border-radius: 15px; |
| 967 | background: qlineargradient(x1:0, y1:0, x2:0, y2:1, |
| 968 | stop:0 rgba(46, 204, 113, 0.2), |
| 969 | stop:1 rgba(39, 174, 96, 0.1)); |
| 970 | font-weight: 600; |
| 971 | } |
| 972 | """) |
| 973 | return |
| 974 | event.ignore() |
| 975 | |
| 976 | def dragLeaveEvent(self, event): |
| 977 | """Handle drag leave event""" |
| 978 | self.drag_drop_label.setStyleSheet(""" |
| 979 | QLabel { |
| 980 | font-size: 18px; |
| 981 | color: #7f8c8d; |
| 982 | padding: 30px; |
| 983 | border: 3px dashed #bdc3c7; |
| 984 | border-radius: 15px; |
| 985 | background: qlineargradient(x1:0, y1:0, x2:0, y2:1, |
| 986 | stop:0 rgba(236, 240, 241, 0.8), |
| 987 | stop:1 rgba(189, 195, 199, 0.3)); |
| 988 | font-weight: 500; |
| 989 | } |
| 990 | """) |
| 991 | event.accept() |
| 992 | |
| 993 | def dropEvent(self, event): |
| 994 | """Handle drop event""" |
| 995 | files = [url.toLocalFile() for url in event.mimeData().urls() |
| 996 | if url.toLocalFile().lower().endswith('.pkg')] |
| 997 | |
| 998 | if files: |
| 999 | self.load_pkg(files[0]) |
| 1000 | |
| 1001 | if len(files) > 1: |
| 1002 | QMessageBox.information(self, "Multiple files", |
| 1003 | "Multiple PKG files were dragged. Only the first file will be loaded.") |
| 1004 | |
| 1005 | self.drag_drop_label.setStyleSheet(""" |
| 1006 | QLabel { |
| 1007 | font-size: 18px; |
| 1008 | color: #7f8c8d; |
| 1009 | padding: 30px; |
| 1010 | border: 3px dashed #bdc3c7; |
| 1011 | border-radius: 15px; |
| 1012 | background: qlineargradient(x1:0, y1:0, x2:0, y2:1, |
| 1013 | stop:0 rgba(236, 240, 241, 0.8), |
| 1014 | stop:1 rgba(189, 195, 199, 0.3)); |
| 1015 | font-weight: 500; |
| 1016 | } |
| 1017 | """) |
| 1018 | |
| 1019 | event.acceptProposedAction() |
| 1020 | |
| 1021 | def load_pkg(self, pkg_path): |
| 1022 | """Load PKG file""" |
| 1023 | try: |
| 1024 | # Chiudi il package precedente se esiste |
| 1025 | if self.package: |
| 1026 | try: |
| 1027 | if hasattr(self.package, 'close'): |
| 1028 | self.package.close() |
| 1029 | self.package = None |
| 1030 | Logger.log_information("Previous package closed") |
| 1031 | except Exception as e: |
| 1032 | Logger.log_error(f"Error closing previous package: {str(e)}") |
| 1033 | |
| 1034 | # Determine package type and load it |
| 1035 | with open(pkg_path, "rb") as fp: |
| 1036 | magic = struct.unpack(">I", fp.read(4))[0] |
| 1037 | if magic == PackagePS4.MAGIC_PS4: |
| 1038 | self.package = PackagePS4(pkg_path) |
| 1039 | Logger.log_information("PS4 PKG detected") |
| 1040 | elif magic == PackagePS5.MAGIC_PS5: |
| 1041 | self.package = PackagePS5(pkg_path) |
| 1042 | Logger.log_information("PS5 PKG detected") |
| 1043 | elif magic == PackagePS3.MAGIC_PS3: |
| 1044 | self.package = PackagePS3(pkg_path) |
| 1045 | Logger.log_information("PS3 PKG detected") |
| 1046 | else: |
| 1047 | raise ValueError(f"Unknown PKG format: {magic:08X}") |
| 1048 | |
| 1049 | # Update UI |
| 1050 | self.pkg_entry.setText(pkg_path) |
| 1051 | self.load_pkg_icon() |
| 1052 | |
| 1053 | # Update file browser and wallpaper viewer |
| 1054 | if hasattr(self, 'file_browser'): |
| 1055 | self.file_browser.load_files(self.package) |
| 1056 | if hasattr(self, 'wallpaper_viewer'): |
| 1057 | self.wallpaper_viewer.load_wallpapers(self.package) |
| 1058 | |
| 1059 | # Update info tab |
| 1060 | info_dict = self.package.get_info() |
| 1061 | self.update_info(info_dict) |
| 1062 | |
| 1063 | # Cerca e carica automaticamente i file dei trofei |
| 1064 | self.load_trophy_files() |
| 1065 | |
| 1066 | Logger.log_information(f"PKG file loaded successfully: {pkg_path}") |
| 1067 | |
| 1068 | except Exception as e: |
| 1069 | error_msg = f"Error loading PKG file: {str(e)}" |
| 1070 | Logger.log_error(error_msg) |
| 1071 | QMessageBox.critical(self, "Error", error_msg) |
| 1072 | |
| 1073 | # Reset UI state |
| 1074 | self.package = None |
| 1075 | self.image_label.clear() |
| 1076 | self.content_id_label.clear() |
| 1077 | if hasattr(self, 'file_browser'): |
| 1078 | self.file_browser.clear() |
| 1079 | if hasattr(self, 'wallpaper_viewer'): |
| 1080 | self.wallpaper_viewer.clear_viewer() |
| 1081 | |
| 1082 | def load_trophy_files(self): |
| 1083 | """Cerca e carica automaticamente i file dei trofei""" |
| 1084 | try: |
| 1085 | if not self.package: |
| 1086 | return |
| 1087 | |
| 1088 | # Cerca file .trp o .ucp |
| 1089 | trophy_files = [ |
| 1090 | f for f in self.package.files.values() |
| 1091 | if isinstance(f.get("name"), str) and |
| 1092 | (f["name"].lower().endswith('.trp') or f["name"].lower().endswith('.ucp')) |
| 1093 | ] |
| 1094 | |
| 1095 | if trophy_files: |
| 1096 | # Estrai il primo file dei trofei trovato in una directory temporanea |
| 1097 | temp_dir = os.path.join(self.temp_directory, "trophies") |
| 1098 | os.makedirs(temp_dir, exist_ok=True) |
| 1099 | |
| 1100 | trophy_file = trophy_files[0] |
| 1101 | temp_path = os.path.join(temp_dir, os.path.basename(trophy_file["name"])) |
| 1102 | |
| 1103 | # Estrai il file |
| 1104 | with open(temp_path, "wb") as f: |
| 1105 | data = self.package.read_file(trophy_file["id"]) |
| 1106 | f.write(data) |
| 1107 | |
| 1108 | # Carica il file nella sezione trofei |
| 1109 | self.trophy_entry.setText(temp_path) |
| 1110 | trophy_reader = TRPReader(temp_path) |
| 1111 | |
| 1112 | # Mostra le informazioni nel text edit |
| 1113 | info_text = f""" |
| 1114 | Title: {trophy_reader._title if trophy_reader._title else 'N/A'} |
| 1115 | NP Communication ID: {trophy_reader._npcommid if trophy_reader._npcommid else 'N/A'} |
| 1116 | Number of Trophies: {len(trophy_reader._trophyList) if trophy_reader._trophyList else 0} |
| 1117 | """ |
| 1118 | self.trophy_info.setText(info_text) |
| 1119 | |
| 1120 | # Carica i trofei nella tree view |
| 1121 | self.trophy_tree.clear() |
| 1122 | for trophy in trophy_reader._trophyList: |
| 1123 | item = QTreeWidgetItem(self.trophy_tree) |
| 1124 | item.setText(0, trophy.name) |
| 1125 | item.setText(1, self.get_trophy_type(trophy)) |
| 1126 | item.setText(2, self.get_trophy_grade(trophy)) |
| 1127 | item.setText(3, "No") # Hidden di default |
| 1128 | item.setData(0, Qt.UserRole, trophy) |
| 1129 | |
| 1130 | # Passa alla tab dei trofei |
| 1131 | self.tab_widget.setCurrentWidget(self.trophy_tab) |
| 1132 | |
| 1133 | Logger.log_information(f"Trophy file loaded: {trophy_file['name']}") |
| 1134 | |
| 1135 | except Exception as e: |
| 1136 | Logger.log_error(f"Error loading trophy files: {str(e)}") |
| 1137 | |
| 1138 | def load_pkg_icon(self): |
| 1139 | """Load and display PKG icon""" |
| 1140 | try: |
| 1141 | # Get content ID |
| 1142 | content_id = self.get_content_id() |
| 1143 | if content_id: |
| 1144 | self.content_id_label.setText(f"Content ID: {content_id}") |
| 1145 | |
| 1146 | # Find icon file |
| 1147 | icon_file = next((f for f in self.package.files.values() |
| 1148 | if isinstance(f, dict) and |
| 1149 | f.get('name', '').lower() in ['icon0.png', 'ICON0.PNG']), None) |
| 1150 | |
| 1151 | if icon_file: |
| 1152 | # Load and display icon |
| 1153 | icon_data = self.package.read_file(icon_file['id']) |
| 1154 | pixmap = ImageUtils.create_thumbnail(icon_data) |
| 1155 | self.image_label.setPixmap(pixmap) |
| 1156 | self.image_label.setAlignment(Qt.AlignCenter) |
| 1157 | |
| 1158 | except Exception as e: |
| 1159 | logging.error(f"Error loading PKG icon: {str(e)}") |
| 1160 | self.image_label.setText("Error loading icon") |
| 1161 | |
| 1162 | def get_content_id(self): |
| 1163 | """Get content ID from package""" |
| 1164 | try: |
| 1165 | if not self.package: |
| 1166 | return None |
| 1167 | |
| 1168 | if isinstance(self.package, PackagePS3): |
| 1169 | return getattr(self.package, 'content_id', None) |
| 1170 | elif isinstance(self.package, (PackagePS4, PackagePS5)): |
| 1171 | return getattr(self.package, 'pkg_content_id', None) |
| 1172 | |
| 1173 | return None |
| 1174 | |
| 1175 | except Exception as e: |
| 1176 | logging.error(f"Error getting content ID: {str(e)}") |
| 1177 | return None |
| 1178 | |
| 1179 | def load_settings(self): |
| 1180 | """Load application settings""" |
| 1181 | try: |
| 1182 | settings = StyleManager.load_settings() |
| 1183 | self.night_mode = settings.get("night_mode", False) |
| 1184 | StyleManager.apply_theme(self, settings) |
| 1185 | except Exception as e: |
| 1186 | logging.error(f"Error loading settings: {str(e)}") |
| 1187 | |
| 1188 | def setup_info_tab(self): |
| 1189 | """Setup the info tab""" |
| 1190 | layout = QVBoxLayout(self.info_tab) |
| 1191 | |
| 1192 | # Tree widget for info display |
| 1193 | self.info_tree = QTreeWidget() |
| 1194 | self.info_tree.setHeaderLabels(["Key", "Value", "Description"]) |
| 1195 | self.info_tree.setColumnWidth(0, 200) |
| 1196 | self.info_tree.setColumnWidth(1, 200) |
| 1197 | layout.addWidget(self.info_tree) |
| 1198 | |
| 1199 | def setup_extract_tab(self): |
| 1200 | """Setup the extract tab""" |
| 1201 | layout = QVBoxLayout(self.extract_tab) |
| 1202 | |
| 1203 | # Output path selection |
| 1204 | output_layout = QHBoxLayout() |
| 1205 | self.extract_out_entry = QLineEdit() |
| 1206 | self.extract_out_entry.setPlaceholderText("Select output directory") |
| 1207 | browse_button = QPushButton("Browse") |
| 1208 | browse_button.clicked.connect(lambda: self.browse_directory(self.extract_out_entry)) |
| 1209 | output_layout.addWidget(self.extract_out_entry) |
| 1210 | output_layout.addWidget(browse_button) |
| 1211 | layout.addLayout(output_layout) |
| 1212 | |
| 1213 | # PFS Info controls |
| 1214 | pfs_controls = QHBoxLayout() |
| 1215 | self.pfs_info_button = QPushButton("PFS Info (shadPKG)") |
| 1216 | self.pfs_info_button.setToolTip("Show PFS structure without extracting files") |
| 1217 | self.pfs_info_button.clicked.connect(self.run_pfs_info) |
| 1218 | pfs_controls.addWidget(self.pfs_info_button) |
| 1219 | pfs_controls.addStretch(1) |
| 1220 | layout.addLayout(pfs_controls) |
| 1221 | |
| 1222 | # PFS Info output |
| 1223 | self.pfs_info_view = QTextEdit() |
| 1224 | self.pfs_info_view.setReadOnly(True) |
| 1225 | self.pfs_info_view.setPlaceholderText("Output PFS Info (shadPKG)") |
| 1226 | layout.addWidget(self.pfs_info_view) |
| 1227 | |
| 1228 | # Extract log |
| 1229 | self.extract_log = QTextEdit() |
| 1230 | self.extract_log.setReadOnly(True) |
| 1231 | layout.addWidget(self.extract_log) |
| 1232 | |
| 1233 | # Extract button |
| 1234 | extract_button = QPushButton("Extract") |
| 1235 | extract_button.clicked.connect(self.extract_pkg) |
| 1236 | layout.addWidget(extract_button) |
| 1237 | |
| 1238 | def setup_inject_tab(self): |
| 1239 | """Setup the inject tab (Work in Progress placeholder)""" |
| 1240 | layout = QVBoxLayout(self.inject_tab) |
| 1241 | wip = QLabel("🚧 Inject - Work in progress") |
| 1242 | wip.setAlignment(Qt.AlignCenter) |
| 1243 | wip.setStyleSheet(""" |
| 1244 | QLabel { |
| 1245 | font-size: 18px; |
| 1246 | font-weight: 700; |
| 1247 | color: #2d3748; |
| 1248 | padding: 24px; |
| 1249 | background: rgba(255, 255, 255, 12%); |
| 1250 | border: 1px solid rgba(0,0,0,8%); |
| 1251 | border-radius: 12px; |
| 1252 | } |
| 1253 | """) |
| 1254 | layout.addStretch(1) |
| 1255 | layout.addWidget(wip) |
| 1256 | layout.addStretch(1) |
| 1257 | |
| 1258 | def setup_modify_tab(self): |
| 1259 | """Setup the modify tab""" |
| 1260 | layout = QVBoxLayout(self.modify_tab) |
| 1261 | |
| 1262 | # Hex viewer |
| 1263 | self.hex_viewer = QTextEdit() |
| 1264 | self.hex_viewer.setReadOnly(True) |
| 1265 | self.hex_viewer.setFont(QFont("Courier")) |
| 1266 | layout.addWidget(self.hex_viewer) |
| 1267 | |
| 1268 | # Offset and data entry |
| 1269 | offset_layout = QHBoxLayout() |
| 1270 | self.offset_entry = QLineEdit() |
| 1271 | self.offset_entry.setPlaceholderText("Offset (hex)") |
| 1272 | self.data_entry = QLineEdit() |
| 1273 | self.data_entry.setPlaceholderText("New data (hex)") |
| 1274 | offset_layout.addWidget(QLabel("Offset:")) |
| 1275 | offset_layout.addWidget(self.offset_entry) |
| 1276 | offset_layout.addWidget(QLabel("Data:")) |
| 1277 | offset_layout.addWidget(self.data_entry) |
| 1278 | layout.addLayout(offset_layout) |
| 1279 | |
| 1280 | # Modify button |
| 1281 | modify_button = QPushButton("Modify") |
| 1282 | modify_button.clicked.connect(self.modify_pkg) |
| 1283 | layout.addWidget(modify_button) |
| 1284 | |
| 1285 | def setup_trophy_tab(self): |
| 1286 | """Setup the trophy tab""" |
| 1287 | layout = QVBoxLayout(self.trophy_tab) |
| 1288 | |
| 1289 | # File selection with better styling |
| 1290 | file_group = QGroupBox("Trophy File") |
| 1291 | file_layout = QHBoxLayout() |
| 1292 | |
| 1293 | self.trophy_entry = QLineEdit() |
| 1294 | self.trophy_entry.setPlaceholderText("Select trophy file (.trp)") |
| 1295 | self.trophy_entry.setStyleSheet(""" |
| 1296 | QLineEdit { |
| 1297 | padding: 8px; |
| 1298 | border: 2px solid #3498db; |
| 1299 | border-radius: 15px; |
| 1300 | font-size: 14px; |
| 1301 | } |
| 1302 | """) |
| 1303 | |
| 1304 | browse_button = QPushButton("Browse") |
| 1305 | browse_button.setStyleSheet(""" |
| 1306 | QPushButton { |
| 1307 | padding: 8px 15px; |
| 1308 | background: #3498db; |
| 1309 | color: white; |
| 1310 | border: none; |
| 1311 | border-radius: 15px; |
| 1312 | font-weight: bold; |
| 1313 | } |
| 1314 | QPushButton:hover { |
| 1315 | background: #2980b9; |
| 1316 | } |
| 1317 | """) |
| 1318 | browse_button.clicked.connect(self.browse_trophy) |
| 1319 | |
| 1320 | file_layout.addWidget(self.trophy_entry) |
| 1321 | file_layout.addWidget(browse_button) |
| 1322 | file_group.setLayout(file_layout) |
| 1323 | layout.addWidget(file_group) |
| 1324 | |
| 1325 | # Split view for trophy list and preview |
| 1326 | split_layout = QHBoxLayout() |
| 1327 | |
| 1328 | # Left side: Trophy list and info |
| 1329 | left_panel = QVBoxLayout() |
| 1330 | |
| 1331 | # Trophy info display |
| 1332 | self.trophy_info = QTextEdit() |
| 1333 | self.trophy_info.setReadOnly(True) |
| 1334 | self.trophy_info.setMaximumHeight(100) |
| 1335 | self.trophy_info.setStyleSheet(""" |
| 1336 | QTextEdit { |
| 1337 | background-color: white; |
| 1338 | border: 1px solid #3498db; |
| 1339 | border-radius: 5px; |
| 1340 | padding: 5px; |
| 1341 | font-size: 13px; |
| 1342 | } |
| 1343 | """) |
| 1344 | left_panel.addWidget(self.trophy_info) |
| 1345 | |
| 1346 | # Trophy list |
| 1347 | self.trophy_tree = QTreeWidget() |
| 1348 | self.trophy_tree.setHeaderLabels(["Trophy", "Type", "Grade", "Hidden"]) |
| 1349 | self.trophy_tree.setStyleSheet(""" |
| 1350 | QTreeWidget { |
| 1351 | border: 1px solid #3498db; |
| 1352 | border-radius: 5px; |
| 1353 | } |
| 1354 | QTreeWidget::item { |
| 1355 | padding: 5px; |
| 1356 | } |
| 1357 | QTreeWidget::item:selected { |
| 1358 | background-color: #3498db; |
| 1359 | color: white; |
| 1360 | } |
| 1361 | """) |
| 1362 | self.trophy_tree.itemClicked.connect(self.display_selected_trophy) |
| 1363 | left_panel.addWidget(self.trophy_tree) |
| 1364 | |
| 1365 | # Right side: Trophy image and details |
| 1366 | right_panel = QVBoxLayout() |
| 1367 | |
| 1368 | # Trophy image viewer |
| 1369 | self.trophy_image_viewer = QLabel() |
| 1370 | self.trophy_image_viewer.setAlignment(Qt.AlignCenter) |
| 1371 | self.trophy_image_viewer.setStyleSheet(""" |
| 1372 | QLabel { |
| 1373 | background-color: white; |
| 1374 | border: 1px solid #3498db; |
| 1375 | border-radius: 5px; |
| 1376 | min-height: 300px; |
| 1377 | } |
| 1378 | """) |
| 1379 | right_panel.addWidget(self.trophy_image_viewer) |
| 1380 | |
| 1381 | # Trophy details |
| 1382 | self.trophy_details = QTextEdit() |
| 1383 | self.trophy_details.setReadOnly(True) |
| 1384 | self.trophy_details.setMaximumHeight(150) |
| 1385 | self.trophy_details.setStyleSheet(self.trophy_info.styleSheet()) |
| 1386 | right_panel.addWidget(self.trophy_details) |
| 1387 | |
| 1388 | # Navigation buttons |
| 1389 | nav_layout = QHBoxLayout() |
| 1390 | self.prev_trophy_button = QPushButton("Previous") |
| 1391 | self.next_trophy_button = QPushButton("Next") |
| 1392 | |
| 1393 | for button in [self.prev_trophy_button, self.next_trophy_button]: |
| 1394 | button.setStyleSheet(browse_button.styleSheet()) |
| 1395 | |
| 1396 | self.prev_trophy_button.clicked.connect(self.show_previous_trophy) |
| 1397 | self.next_trophy_button.clicked.connect(self.show_next_trophy) |
| 1398 | |
| 1399 | nav_layout.addWidget(self.prev_trophy_button) |
| 1400 | nav_layout.addWidget(self.next_trophy_button) |
| 1401 | right_panel.addLayout(nav_layout) |
| 1402 | |
| 1403 | # Add panels to split layout |
| 1404 | split_layout.addLayout(left_panel) |
| 1405 | split_layout.addLayout(right_panel) |
| 1406 | layout.addLayout(split_layout) |
| 1407 | |
| 1408 | # Action buttons |
| 1409 | button_layout = QHBoxLayout() |
| 1410 | |
| 1411 | self.trophy_edit_button = QPushButton("Edit Trophy") |
| 1412 | self.trophy_recompile_button = QPushButton("Recompile TRP") |
| 1413 | self.trophy_decrypt_button = QPushButton("Decrypt Trophy") |
| 1414 | |
| 1415 | for button in [self.trophy_edit_button, self.trophy_recompile_button, self.trophy_decrypt_button]: |
| 1416 | button.setStyleSheet(browse_button.styleSheet()) |
| 1417 | |
| 1418 | self.trophy_edit_button.clicked.connect(self.edit_trophy_info) |
| 1419 | self.trophy_recompile_button.clicked.connect(self.recompile_trp) |
| 1420 | self.trophy_decrypt_button.clicked.connect(self.decrypt_trophy) |
| 1421 | |
| 1422 | button_layout.addWidget(self.trophy_edit_button) |
| 1423 | button_layout.addWidget(self.trophy_recompile_button) |
| 1424 | button_layout.addWidget(self.trophy_decrypt_button) |
| 1425 | |
| 1426 | layout.addLayout(button_layout) |
| 1427 | |
| 1428 | def setup_esmf_decrypter_tab(self): |
| 1429 | """Setup the ESMF decrypter tab""" |
| 1430 | layout = QVBoxLayout(self.esmf_decrypter_tab) |
| 1431 | |
| 1432 | # File selection |
| 1433 | file_layout = QHBoxLayout() |
| 1434 | self.esmf_file_entry = QLineEdit() |
| 1435 | self.esmf_file_entry.setPlaceholderText("Select ESMF file") |
| 1436 | browse_button = QPushButton("Browse") |
| 1437 | browse_button.clicked.connect(lambda: self.browse_file(self.esmf_file_entry, "ESMF files (*.ESMF)")) |
| 1438 | file_layout.addWidget(self.esmf_file_entry) |
| 1439 | file_layout.addWidget(browse_button) |
| 1440 | layout.addLayout(file_layout) |
| 1441 | |
| 1442 | # Output directory |
| 1443 | output_layout = QHBoxLayout() |
| 1444 | self.esmf_output_entry = QLineEdit() |
| 1445 | self.esmf_output_entry.setPlaceholderText("Select output directory") |
| 1446 | output_browse = QPushButton("Browse") |
| 1447 | output_browse.clicked.connect(lambda: self.browse_directory(self.esmf_output_entry)) |
| 1448 | output_layout.addWidget(self.esmf_output_entry) |
| 1449 | output_layout.addWidget(output_browse) |
| 1450 | layout.addLayout(output_layout) |
| 1451 | |
| 1452 | # Decrypt button |
| 1453 | decrypt_button = QPushButton("Decrypt") |
| 1454 | decrypt_button.clicked.connect(self.decrypt_esmf) |
| 1455 | layout.addWidget(decrypt_button) |
| 1456 | |
| 1457 | # Log display |
| 1458 | self.esmf_log = QTextEdit() |
| 1459 | self.esmf_log.setReadOnly(True) |
| 1460 | layout.addWidget(self.esmf_log) |
| 1461 | |
| 1462 | def setup_ps5_game_info_tab(self): |
| 1463 | """Setup the PS5 game info tab""" |
| 1464 | layout = QVBoxLayout(self.ps5_game_info_tab) |
| 1465 | |
| 1466 | # File selection with better styling |
| 1467 | file_group = QGroupBox("File Selection") |
| 1468 | file_layout = QHBoxLayout() |
| 1469 | |
| 1470 | self.ps5_game_path_entry = QLineEdit() |
| 1471 | self.ps5_game_path_entry.setPlaceholderText("Select eboot.bin or param.json file") |
| 1472 | self.ps5_game_path_entry.setStyleSheet(""" |
| 1473 | QLineEdit { |
| 1474 | padding: 8px; |
| 1475 | border: 2px solid #3498db; |
| 1476 | border-radius: 15px; |
| 1477 | font-size: 14px; |
| 1478 | } |
| 1479 | """) |
| 1480 | |
| 1481 | browse_button = QPushButton("Browse") |
| 1482 | browse_button.setStyleSheet(""" |
| 1483 | QPushButton { |
| 1484 | padding: 8px 15px; |
| 1485 | background: #3498db; |
| 1486 | color: white; |
| 1487 | border: none; |
| 1488 | border-radius: 15px; |
| 1489 | font-weight: bold; |
| 1490 | } |
| 1491 | QPushButton:hover { |
| 1492 | background: #2980b9; |
| 1493 | } |
| 1494 | """) |
| 1495 | browse_button.clicked.connect(self.browse_ps5_game_file) |
| 1496 | |
| 1497 | file_layout.addWidget(self.ps5_game_path_entry) |
| 1498 | file_layout.addWidget(browse_button) |
| 1499 | file_group.setLayout(file_layout) |
| 1500 | layout.addWidget(file_group) |
| 1501 | |
| 1502 | # Info table with better styling |
| 1503 | self.ps5_game_info_table = QTableWidget() |
| 1504 | self.ps5_game_info_table.setColumnCount(2) |
| 1505 | self.ps5_game_info_table.setHorizontalHeaderLabels(["Parameter", "Value"]) |
| 1506 | self.ps5_game_info_table.horizontalHeader().setStretchLastSection(True) |
| 1507 | self.ps5_game_info_table.setStyleSheet(""" |
| 1508 | QTableWidget { |
| 1509 | border: 1px solid #bdc3c7; |
| 1510 | border-radius: 5px; |
| 1511 | gridline-color: #ecf0f1; |
| 1512 | } |
| 1513 | QHeaderView::section { |
| 1514 | background-color: #3498db; |
| 1515 | color: white; |
| 1516 | padding: 8px; |
| 1517 | border: none; |
| 1518 | } |
| 1519 | QTableWidget::item { |
| 1520 | padding: 5px; |
| 1521 | } |
| 1522 | QTableWidget::item:selected { |
| 1523 | background-color: #e8f0fe; |
| 1524 | color: #2c3e50; |
| 1525 | } |
| 1526 | """) |
| 1527 | layout.addWidget(self.ps5_game_info_table) |
| 1528 | |
| 1529 | # Control buttons |
| 1530 | button_layout = QHBoxLayout() |
| 1531 | save_button = QPushButton("Save Changes") |
| 1532 | reload_button = QPushButton("Reload") |
| 1533 | |
| 1534 | for button in [save_button, reload_button]: |
| 1535 | button.setStyleSheet(""" |
| 1536 | QPushButton { |
| 1537 | padding: 8px 20px; |
| 1538 | background: #3498db; |
| 1539 | color: white; |
| 1540 | border: none; |
| 1541 | border-radius: 15px; |
| 1542 | font-weight: bold; |
| 1543 | min-width: 120px; |
| 1544 | } |
| 1545 | QPushButton:hover { |
| 1546 | background: #2980b9; |
| 1547 | } |
| 1548 | """) |
| 1549 | |
| 1550 | save_button.clicked.connect(self.save_ps5_game_info) |
| 1551 | reload_button.clicked.connect(self.reload_ps5_game_info) |
| 1552 | |
| 1553 | button_layout.addStretch() |
| 1554 | button_layout.addWidget(save_button) |
| 1555 | button_layout.addWidget(reload_button) |
| 1556 | button_layout.addStretch() |
| 1557 | |
| 1558 | layout.addLayout(button_layout) |
| 1559 | |
| 1560 | def browse_ps5_game_file(self): |
| 1561 | """Browse for PS5 game file""" |
| 1562 | filename, _ = QFileDialog.getOpenFileName( |
| 1563 | self, |
| 1564 | "Select eboot.bin or param.json", |
| 1565 | "", |
| 1566 | "PS5 Game Files (eboot.bin param.json);;All files (*.*)" |
| 1567 | ) |
| 1568 | if filename: |
| 1569 | self.ps5_game_path_entry.setText(filename) |
| 1570 | self.load_ps5_game_info(filename) |
| 1571 | |
| 1572 | def load_ps5_game_info(self, file_path): |
| 1573 | """Load PS5 game info""" |
| 1574 | try: |
| 1575 | # Create PS5GameInfo instance |
| 1576 | self.ps5_game_info = PS5GameInfo() |
| 1577 | |
| 1578 | # Process the directory containing the file |
| 1579 | directory = os.path.dirname(file_path) |
| 1580 | info = self.ps5_game_info.process(directory) |
| 1581 | |
| 1582 | # Clear and resize table |
| 1583 | self.ps5_game_info_table.setRowCount(0) |
| 1584 | |
| 1585 | # Add info to table |
| 1586 | for key, value in info.items(): |
| 1587 | row = self.ps5_game_info_table.rowCount() |
| 1588 | self.ps5_game_info_table.insertRow(row) |
| 1589 | |
| 1590 | # Add key and value |
| 1591 | self.ps5_game_info_table.setItem(row, 0, QTableWidgetItem(str(key))) |
| 1592 | self.ps5_game_info_table.setItem(row, 1, QTableWidgetItem(str(value))) |
| 1593 | |
| 1594 | # Adjust columns |
| 1595 | self.ps5_game_info_table.resizeColumnsToContents() |
| 1596 | |
| 1597 | except Exception as e: |
| 1598 | QMessageBox.critical(self, "Error", f"Failed to load PS5 game info: {str(e)}") |
| 1599 | |
| 1600 | def save_ps5_game_info(self): |
| 1601 | """Save PS5 game info changes""" |
| 1602 | try: |
| 1603 | if not hasattr(self, 'ps5_game_info'): |
| 1604 | QMessageBox.warning(self, "Warning", "No PS5 game info loaded") |
| 1605 | return |
| 1606 | |
| 1607 | # Get file path |
| 1608 | file_path = self.ps5_game_path_entry.text() |
| 1609 | if not file_path: |
| 1610 | QMessageBox.warning(self, "Warning", "No file selected") |
| 1611 | return |
| 1612 | |
| 1613 | # Collect changes from table |
| 1614 | changes = {} |
| 1615 | for row in range(self.ps5_game_info_table.rowCount()): |
| 1616 | key = self.ps5_game_info_table.item(row, 0).text() |
| 1617 | value = self.ps5_game_info_table.item(row, 1).text() |
| 1618 | changes[key] = value |
| 1619 | |
| 1620 | # Update the main_dict in PS5GameInfo |
| 1621 | self.ps5_game_info.main_dict = changes |
| 1622 | |
| 1623 | # Save changes to param.json |
| 1624 | param_json_path = os.path.join(os.path.dirname(file_path), "sce_sys/param.json") |
| 1625 | if os.path.exists(param_json_path): |
| 1626 | with open(param_json_path, "r+") as f: |
| 1627 | existing_data = json.load(f) |
| 1628 | for key, value in changes.items(): |
| 1629 | if key in existing_data: |
| 1630 | existing_data[key] = value |
| 1631 | f.seek(0) |
| 1632 | json.dump(existing_data, f, indent=4) |
| 1633 | f.truncate() |
| 1634 | |
| 1635 | QMessageBox.information(self, "Success", "Changes saved successfully") |
| 1636 | else: |
| 1637 | QMessageBox.warning(self, "Error", "param.json file not found") |
| 1638 | |
| 1639 | except Exception as e: |
| 1640 | QMessageBox.critical(self, "Error", f"Failed to save changes: {str(e)}") |
| 1641 | |
| 1642 | def reload_ps5_game_info(self): |
| 1643 | """Reload PS5 game info""" |
| 1644 | file_path = self.ps5_game_path_entry.text() |
| 1645 | if file_path: |
| 1646 | self.load_ps5_game_info(file_path) |
| 1647 | else: |
| 1648 | QMessageBox.warning(self, "Warning", "No file selected") |
| 1649 | |
| 1650 | def setup_bruteforce_tab(self): |
| 1651 | """Setup the passcode bruteforcer tab""" |
| 1652 | layout = QVBoxLayout(self.bruteforce_tab) |
| 1653 | |
| 1654 | # Output directory selection |
| 1655 | output_layout = QHBoxLayout() |
| 1656 | self.bruteforce_out_entry = QLineEdit() |
| 1657 | self.bruteforce_out_entry.setPlaceholderText("Select output directory") |
| 1658 | browse_button = QPushButton("Browse") |
| 1659 | browse_button.clicked.connect(lambda: self.browse_directory(self.bruteforce_out_entry)) |
| 1660 | output_layout.addWidget(self.bruteforce_out_entry) |
| 1661 | output_layout.addWidget(browse_button) |
| 1662 | layout.addLayout(output_layout) |
| 1663 | |
| 1664 | # Passcode input |
| 1665 | passcode_group = QGroupBox("Passcode") |
| 1666 | passcode_layout = QVBoxLayout() |
| 1667 | |
| 1668 | # Manual passcode input |
| 1669 | manual_layout = QHBoxLayout() |
| 1670 | self.passcode_entry = QLineEdit() |
| 1671 | self.passcode_entry.setPlaceholderText("Enter 32-character passcode (optional)") |
| 1672 | self.passcode_entry.setMaxLength(32) |
| 1673 | manual_layout.addWidget(self.passcode_entry) |
| 1674 | |
| 1675 | # Try passcode button |
| 1676 | try_button = QPushButton("Try Passcode") |
| 1677 | try_button.clicked.connect(self.try_manual_passcode) |
| 1678 | manual_layout.addWidget(try_button) |
| 1679 | |
| 1680 | passcode_layout.addLayout(manual_layout) |
| 1681 | |
| 1682 | # Threads selector, Seed, and Stop button |
| 1683 | control_layout = QHBoxLayout() |
| 1684 | control_layout.addWidget(QLabel("Threads:")) |
| 1685 | self.brute_threads_spin = QSpinBox() |
| 1686 | self.brute_threads_spin.setRange(1, 32) |
| 1687 | self.brute_threads_spin.setValue(1) |
| 1688 | self.brute_threads_spin.setToolTip("Number of parallel workers") |
| 1689 | control_layout.addWidget(self.brute_threads_spin) |
| 1690 | |
| 1691 | control_layout.addWidget(QLabel("Seed:")) |
| 1692 | self.brute_seed_edit = QLineEdit() |
| 1693 | self.brute_seed_edit.setPlaceholderText("optional integer") |
| 1694 | self.brute_seed_edit.setToolTip("Optional integer seed for deterministic traversal") |
| 1695 | self.brute_seed_edit.setMaximumWidth(160) |
| 1696 | control_layout.addWidget(self.brute_seed_edit) |
| 1697 | |
| 1698 | self.brute_stop_button = QPushButton("Stop") |
| 1699 | self.brute_stop_button.setEnabled(False) |
| 1700 | self.brute_stop_button.clicked.connect(self.stop_bruteforce) |
| 1701 | control_layout.addWidget(self.brute_stop_button) |
| 1702 | |
| 1703 | # Reset button |
| 1704 | self.brute_reset_button = QPushButton("Reset") |
| 1705 | self.brute_reset_button.setToolTip("Stop and clear progress (.brutestate/.success)") |
| 1706 | self.brute_reset_button.clicked.connect(self.reset_bruteforce) |
| 1707 | control_layout.addWidget(self.brute_reset_button) |
| 1708 | passcode_layout.addLayout(control_layout) |
| 1709 | |
| 1710 | # Or label |
| 1711 | or_label = QLabel("- OR -") |
| 1712 | or_label.setAlignment(Qt.AlignCenter) |
| 1713 | passcode_layout.addWidget(or_label) |
| 1714 | |
| 1715 | # Bruteforce button |
| 1716 | self.brute_start_button = QPushButton("Start Bruteforce") |
| 1717 | self.brute_start_button.clicked.connect(self.run_bruteforce) |
| 1718 | passcode_layout.addWidget(self.brute_start_button) |
| 1719 | |
| 1720 | passcode_group.setLayout(passcode_layout) |
| 1721 | layout.addWidget(passcode_group) |
| 1722 | |
| 1723 | # Log display |
| 1724 | self.bruteforce_log = QTextEdit() |
| 1725 | self.bruteforce_log.setReadOnly(True) |
| 1726 | layout.addWidget(self.bruteforce_log) |
| 1727 | |
| 1728 | # Live stats labels |
| 1729 | stats_layout = QHBoxLayout() |
| 1730 | self.brute_attempts_label = QLabel("Attempts: 0") |
| 1731 | self.brute_rate_label = QLabel("Rate: 0/s") |
| 1732 | stats_layout.addWidget(self.brute_attempts_label) |
| 1733 | stats_layout.addWidget(self.brute_rate_label) |
| 1734 | stats_layout.addStretch(1) |
| 1735 | layout.addLayout(stats_layout) |
| 1736 | |
| 1737 | # Live tested keys list (bounded) |
| 1738 | tested_group = QGroupBox("Tested Keys (live)") |
| 1739 | tested_layout = QVBoxLayout() |
| 1740 | self.tested_keys_list = QListWidget() |
| 1741 | self.tested_keys_list.setAlternatingRowColors(True) |
| 1742 | tested_layout.addWidget(self.tested_keys_list) |
| 1743 | self.tested_count_label = QLabel("Shown: 0 (max 1000)") |
| 1744 | tested_layout.addWidget(self.tested_count_label) |
| 1745 | tested_group.setLayout(tested_layout) |
| 1746 | layout.addWidget(tested_group) |
| 1747 | |
| 1748 | def try_manual_passcode(self): |
| 1749 | """Try decrypting with manual passcode""" |
| 1750 | if not self.package: |
| 1751 | QMessageBox.warning(self, "Warning", "Please load a PKG file first") |
| 1752 | return |
| 1753 | |
| 1754 | output_dir = self.bruteforce_out_entry.text() |
| 1755 | if not output_dir: |
| 1756 | QMessageBox.warning(self, "Warning", "Please select an output directory") |
| 1757 | return |
| 1758 | |
| 1759 | passcode = self.passcode_entry.text() |
| 1760 | if not passcode: |
| 1761 | QMessageBox.warning(self, "Warning", "Please enter a passcode") |
| 1762 | return |
| 1763 | |
| 1764 | try: |
| 1765 | bruteforcer = PS4PasscodeBruteforcer() |
| 1766 | result = bruteforcer.brute_force_passcode( |
| 1767 | self.package.original_file, |
| 1768 | output_dir, |
| 1769 | lambda msg: self.bruteforce_log.append(msg), |
| 1770 | manual_passcode=passcode |
| 1771 | ) |
| 1772 | self.bruteforce_log.append(result) |
| 1773 | if "successfully" in result.lower(): |
| 1774 | QMessageBox.information(self, "Success", result) |
| 1775 | else: |
| 1776 | QMessageBox.warning(self, "Warning", result) |
| 1777 | except Exception as e: |
| 1778 | error_msg = f"Failed to try passcode: {str(e)}" |
| 1779 | self.bruteforce_log.append(error_msg) |
| 1780 | QMessageBox.critical(self, "Error", error_msg) |
| 1781 | |
| 1782 | def setup_trp_create_tab(self): |
| 1783 | """Setup the TRP creation tab""" |
| 1784 | layout = QVBoxLayout(self.trp_create_tab) |
| 1785 | |
| 1786 | # Trophy info |
| 1787 | info_group = QGroupBox("Trophy Information") |
| 1788 | info_layout = QGridLayout() |
| 1789 | |
| 1790 | # Title input |
| 1791 | self.trp_title_edit = QLineEdit() |
| 1792 | self.trp_title_edit.setPlaceholderText("Enter trophy title") |
| 1793 | info_layout.addWidget(QLabel("Title:"), 0, 0) |
| 1794 | info_layout.addWidget(self.trp_title_edit, 0, 1) |
| 1795 | |
| 1796 | # NPCommID input |
| 1797 | self.trp_npcommid_edit = QLineEdit() |
| 1798 | self.trp_npcommid_edit.setPlaceholderText("Enter NPCommID") |
| 1799 | info_layout.addWidget(QLabel("NPCommID:"), 1, 0) |
| 1800 | info_layout.addWidget(self.trp_npcommid_edit, 1, 1) |
| 1801 | |
| 1802 | # Trophy count |
| 1803 | self.trp_trophy_count = QSpinBox() |
| 1804 | self.trp_trophy_count.setRange(1, 100) |
| 1805 | self.trp_trophy_count.setValue(1) |
| 1806 | info_layout.addWidget(QLabel("Trophy Count:"), 2, 0) |
| 1807 | info_layout.addWidget(self.trp_trophy_count, 2, 1) |
| 1808 | |
| 1809 | info_group.setLayout(info_layout) |
| 1810 | layout.addWidget(info_group) |
| 1811 | |
| 1812 | # File list |
| 1813 | files_group = QGroupBox("Trophy Files") |
| 1814 | files_layout = QVBoxLayout() |
| 1815 | |
| 1816 | self.trophy_files_list = QTreeWidget() |
| 1817 | self.trophy_files_list.setHeaderLabels(["Name", "Size"]) |
| 1818 | files_layout.addWidget(self.trophy_files_list) |
| 1819 | |
| 1820 | # Add file button |
| 1821 | add_file_button = QPushButton("Add Trophy Files") |
| 1822 | add_file_button.clicked.connect(self.add_trophy_files) |
| 1823 | files_layout.addWidget(add_file_button) |
| 1824 | |
| 1825 | files_group.setLayout(files_layout) |
| 1826 | layout.addWidget(files_group) |
| 1827 | |
| 1828 | # Create button |
| 1829 | create_button = QPushButton("Create TRP") |
| 1830 | create_button.clicked.connect(self.create_trp) |
| 1831 | layout.addWidget(create_button) |
| 1832 | |
| 1833 | # Log display |
| 1834 | self.trp_create_log = QTextEdit() |
| 1835 | self.trp_create_log.setReadOnly(True) |
| 1836 | layout.addWidget(self.trp_create_log) |
| 1837 | |
| 1838 | def add_trophy_files(self): |
| 1839 | """Add trophy files to the list""" |
| 1840 | files, _ = QFileDialog.getOpenFileNames( |
| 1841 | self, |
| 1842 | "Select Trophy Files", |
| 1843 | "", |
| 1844 | "Trophy Files (*.png *.jpg *.jpeg)" |
| 1845 | ) |
| 1846 | |
| 1847 | if files: |
| 1848 | for file_path in files: |
| 1849 | try: |
| 1850 | with open(file_path, 'rb') as f: |
| 1851 | data = f.read() |
| 1852 | |
| 1853 | file_name = os.path.basename(file_path) |
| 1854 | size = len(data) |
| 1855 | |
| 1856 | item = QTreeWidgetItem(self.trophy_files_list) |
| 1857 | item.setText(0, file_name) |
| 1858 | item.setText(1, FileUtils.format_size(size)) |
| 1859 | item.setData(0, Qt.UserRole, { |
| 1860 | 'path': file_path, |
| 1861 | 'data': data, |
| 1862 | 'size': size |
| 1863 | }) |
| 1864 | |
| 1865 | except Exception as e: |
| 1866 | QMessageBox.warning(self, "Error", f"Failed to add file {file_path}: {str(e)}") |
| 1867 | |
| 1868 | def create_trp(self): |
| 1869 | """Create TRP file""" |
| 1870 | if not self.trophy_files_list.topLevelItemCount(): |
| 1871 | QMessageBox.warning(self, "Warning", "Please add trophy files first") |
| 1872 | return |
| 1873 | |
| 1874 | title = self.trp_title_edit.text() |
| 1875 | npcommid = self.trp_npcommid_edit.text() |
| 1876 | |
| 1877 | if not title or not npcommid: |
| 1878 | QMessageBox.warning(self, "Warning", "Please enter title and NPCommID") |
| 1879 | return |
| 1880 | |
| 1881 | try: |
| 1882 | # Get save location |
| 1883 | output_path, _ = QFileDialog.getSaveFileName( |
| 1884 | self, |
| 1885 | "Save TRP File", |
| 1886 | "", |
| 1887 | "TRP files (*.trp)" |
| 1888 | ) |
| 1889 | |
| 1890 | if not output_path: |
| 1891 | return |
| 1892 | |
| 1893 | # Create TRP |
| 1894 | creator = TRPCreator() |
| 1895 | creator.SetVersion = 1 # Imposta la versione a 1 |
| 1896 | |
| 1897 | # Raccogli tutti i file |
| 1898 | files = [] |
| 1899 | root = self.trophy_files_list.invisibleRootItem() |
| 1900 | for i in range(root.childCount()): |
| 1901 | item = root.child(i) |
| 1902 | file_data = item.data(0, Qt.UserRole) |
| 1903 | files.append(file_data['path']) |
| 1904 | |
| 1905 | # Crea il file TRP |
| 1906 | try: |
| 1907 | creator.Create(output_path, files) |
| 1908 | self.trp_create_log.append(f"TRP file created successfully: {output_path}") |
| 1909 | QMessageBox.information(self, "Success", "TRP file created successfully") |
| 1910 | except Exception as e: |
| 1911 | raise Exception(f"Failed to create TRP: {str(e)}") |
| 1912 | |
| 1913 | except Exception as e: |
| 1914 | self.trp_create_log.append(f"Error creating TRP: {str(e)}") |
| 1915 | QMessageBox.critical(self, "Error", f"Failed to create TRP: {str(e)}") |
| 1916 | |
| 1917 | def browse_pkg(self): |
| 1918 | """Browse for PKG file""" |
| 1919 | filename, _ = QFileDialog.getOpenFileName( |
| 1920 | self, |
| 1921 | "Select PKG file", |
| 1922 | "", |
| 1923 | "PKG files (*.pkg)" |
| 1924 | ) |
| 1925 | if filename: |
| 1926 | self.pkg_entry.setText(filename) |
| 1927 | self.load_pkg(filename) |
| 1928 | |
| 1929 | def browse_file(self, entry_widget, file_filter="All files (*.*)"): |
| 1930 | """Browse for file""" |
| 1931 | filename, _ = QFileDialog.getOpenFileName( |
| 1932 | self, |
| 1933 | "Select file", |
| 1934 | "", |
| 1935 | file_filter |
| 1936 | ) |
| 1937 | if filename: |
| 1938 | entry_widget.setText(filename) |
| 1939 | |
| 1940 | def browse_directory(self, entry_widget): |
| 1941 | """Browse for directory""" |
| 1942 | directory = QFileDialog.getExistingDirectory( |
| 1943 | self, |
| 1944 | "Select Directory" |
| 1945 | ) |
| 1946 | if directory: |
| 1947 | entry_widget.setText(directory) |
| 1948 | |
| 1949 | def extract_pkg(self): |
| 1950 | """Extract PKG contents""" |
| 1951 | if not self.package: |
| 1952 | QMessageBox.warning(self, "Warning", "Please load a PKG file first") |
| 1953 | return |
| 1954 | |
| 1955 | output_dir = self.extract_out_entry.text() |
| 1956 | if not output_dir: |
| 1957 | QMessageBox.warning(self, "Warning", "Please select an output directory") |
| 1958 | return |
| 1959 | |
| 1960 | # Run extraction in background to keep UI responsive |
| 1961 | try: |
| 1962 | self.extract_log.append(f"[+] Starting extraction to: {output_dir}") |
| 1963 | |
| 1964 | class ExtractWorker(QObject): |
| 1965 | progress = pyqtSignal(str) |
| 1966 | finished = pyqtSignal(str) |
| 1967 | failed = pyqtSignal(str) |
| 1968 | |
| 1969 | def __init__(self, pkg, out_dir): |
| 1970 | super().__init__() |
| 1971 | self._pkg = pkg |
| 1972 | self._out = out_dir |
| 1973 | |
| 1974 | def run(self): |
| 1975 | try: |
| 1976 | # Prefer shadPKG for PS4, fallback to internal dump |
| 1977 | if isinstance(self._pkg, PackagePS4): |
| 1978 | try: |
| 1979 | result = self._pkg.extract_via_shadpkg(self._out) |
| 1980 | except Exception as e: |
| 1981 | Logger.log_warning(f"shadPKG failed, using internal extraction: {e}") |
| 1982 | self.progress.emit(f"[-] shadPKG failed, using internal extraction: {e}") |
| 1983 | result = self._pkg.dump(self._out) |
| 1984 | else: |
| 1985 | result = self._pkg.dump(self._out) |
| 1986 | self.finished.emit(result) |
| 1987 | except Exception as e: |
| 1988 | self.failed.emit(str(e)) |
| 1989 | |
| 1990 | # Create thread and worker |
| 1991 | self.extract_thread = QThread(self) |
| 1992 | self.extract_worker = ExtractWorker(self.package, output_dir) |
| 1993 | self.extract_worker.moveToThread(self.extract_thread) |
| 1994 | self.extract_thread.started.connect(self.extract_worker.run) |
| 1995 | self.extract_worker.progress.connect(self.extract_log.append) |
| 1996 | |
| 1997 | def on_extract_finished(msg: str): |
| 1998 | try: |
| 1999 | self.extract_log.append(msg) |
| 2000 | QMessageBox.information(self, "Success", "PKG extracted successfully") |
| 2001 | finally: |
| 2002 | self.extract_thread.quit() |
| 2003 | |
| 2004 | def on_extract_failed(err: str): |
| 2005 | try: |
| 2006 | QMessageBox.critical(self, "Error", f"Failed to extract PKG: {err}") |
| 2007 | finally: |
| 2008 | self.extract_thread.quit() |
| 2009 | |
| 2010 | self.extract_worker.finished.connect(on_extract_finished) |
| 2011 | self.extract_worker.failed.connect(on_extract_failed) |
| 2012 | self.extract_thread.finished.connect(self.extract_thread.deleteLater) |
| 2013 | self.extract_thread.start() |
| 2014 | except Exception as e: |
| 2015 | QMessageBox.critical(self, "Error", f"Failed to start extraction: {str(e)}") |
| 2016 | |
| 2017 | |
| 2018 | def inject_file(self): |
| 2019 | """Inject file into PKG (WIP placeholder)""" |
| 2020 | QMessageBox.information(self, "Work in Progress", "The Inject feature is currently under development.") |
| 2021 | return |
| 2022 | |
| 2023 | def run_pfs_info(self): |
| 2024 | """Run shadPKG pfs-info in background and display result""" |
| 2025 | if not self.package: |
| 2026 | QMessageBox.warning(self, "PFS Info", "Please load a PKG file first") |
| 2027 | return |
| 2028 | if not isinstance(self.package, PackagePS4): |
| 2029 | QMessageBox.warning(self, "PFS Info", "PFS Info è disponibile solo per PKG PS4") |
| 2030 | return |
| 2031 | |
| 2032 | # Disable button to prevent multiple runs |
| 2033 | self.pfs_info_button.setEnabled(False) |
| 2034 | self.pfs_info_view.clear() |
| 2035 | self.pfs_info_view.append("[+] Running shadPKG pfs-info...\n") |
| 2036 | |
| 2037 | class PfsInfoWorker(QObject): |
| 2038 | finished = pyqtSignal(str) |
| 2039 | failed = pyqtSignal(str) |
| 2040 | |
| 2041 | def __init__(self, pkg): |
| 2042 | super().__init__() |
| 2043 | self._pkg = pkg |
| 2044 | |
| 2045 | def run(self): |
| 2046 | try: |
| 2047 | output = self._pkg.get_pfs_info(as_json=False) |
| 2048 | self.finished.emit(output) |
| 2049 | except Exception as e: |
| 2050 | self.failed.emit(str(e)) |
| 2051 | |
| 2052 | try: |
| 2053 | self.pfs_thread = QThread(self) |
| 2054 | self.pfs_worker = PfsInfoWorker(self.package) |
| 2055 | self.pfs_worker.moveToThread(self.pfs_thread) |
| 2056 | self.pfs_thread.started.connect(self.pfs_worker.run) |
| 2057 | |
| 2058 | def on_done(text: str): |
| 2059 | try: |
| 2060 | self.pfs_info_view.clear() |
| 2061 | self.pfs_info_view.append(text or "<no output>") |
| 2062 | finally: |
| 2063 | self.pfs_thread.quit() |
| 2064 | self.pfs_info_button.setEnabled(True) |
| 2065 | |
| 2066 | def on_fail(err: str): |
| 2067 | try: |
| 2068 | QMessageBox.critical(self, "PFS Info", f"Failed: {err}") |
| 2069 | finally: |
| 2070 | self.pfs_thread.quit() |
| 2071 | self.pfs_info_button.setEnabled(True) |
| 2072 | |
| 2073 | self.pfs_worker.finished.connect(on_done) |
| 2074 | self.pfs_worker.failed.connect(on_fail) |
| 2075 | self.pfs_thread.finished.connect(self.pfs_thread.deleteLater) |
| 2076 | self.pfs_thread.start() |
| 2077 | except Exception as e: |
| 2078 | self.pfs_info_button.setEnabled(True) |
| 2079 | QMessageBox.critical(self, "PFS Info", f"Failed to start pfs-info: {e}") |
| 2080 | |
| 2081 | def modify_pkg(self): |
| 2082 | """Modify PKG header""" |
| 2083 | if not self.package: |
| 2084 | QMessageBox.warning(self, "Warning", "Please load a PKG file first") |
| 2085 | return |
| 2086 | |
| 2087 | offset = self.offset_entry.text() |
| 2088 | new_data = self.data_entry.text() |
| 2089 | |
| 2090 | if not offset or not new_data: |
| 2091 | QMessageBox.warning(self, "Warning", "Please specify both offset and new data") |
| 2092 | return |
| 2093 | |
| 2094 | try: |
| 2095 | offset = int(offset, 16) |
| 2096 | new_data = bytes.fromhex(new_data) |
| 2097 | result = modify_file_header(self.package.original_file, offset, new_data) |
| 2098 | QMessageBox.information(self, "Success", f"Modified {result} bytes") |
| 2099 | self.update_hex_view() |
| 2100 | except Exception as e: |
| 2101 | QMessageBox.critical(self, "Error", f"Failed to modify PKG: {str(e)}") |
| 2102 | |
| 2103 | def decrypt_esmf(self): |
| 2104 | """Decrypt ESMF file""" |
| 2105 | esmf_file = self.esmf_file_entry.text() |
| 2106 | output_dir = self.esmf_output_entry.text() |
| 2107 | |
| 2108 | if not esmf_file or not output_dir: |
| 2109 | QMessageBox.warning(self, "Warning", "Please select ESMF file and output directory") |
| 2110 | return |
| 2111 | |
| 2112 | try: |
| 2113 | decrypter = ESMFDecrypter() |
| 2114 | result = decrypter.decrypt_esmf(esmf_file, output_dir) |
| 2115 | self.esmf_log.append(result) |
| 2116 | QMessageBox.information(self, "Success", "ESMF decrypted successfully") |
| 2117 | except Exception as e: |
| 2118 | QMessageBox.critical(self, "Error", f"Failed to decrypt ESMF: {str(e)}") |
| 2119 | |
| 2120 | def run_bruteforce(self): |
| 2121 | """Run passcode bruteforcer""" |
| 2122 | if not self.package: |
| 2123 | QMessageBox.warning(self, "Warning", "Please load a PKG file first") |
| 2124 | return |
| 2125 | |
| 2126 | output_dir = self.bruteforce_out_entry.text() |
| 2127 | if not output_dir: |
| 2128 | QMessageBox.warning(self, "Warning", "Please select an output directory") |
| 2129 | return |
| 2130 | |
| 2131 | # Start background bruteforce in QThread |
| 2132 | try: |
| 2133 | # Prepare UI state |
| 2134 | self.brute_start_button.setEnabled(False) |
| 2135 | self.brute_stop_button.setEnabled(True) |
| 2136 | self.bruteforce_log.clear() |
| 2137 | self.tested_keys_list.clear() |
| 2138 | self.tested_count_label.setText("Shown: 0 (max 1000)") |
| 2139 | |
| 2140 | # Create bruteforcer and thread |
| 2141 | self.bruteforcer = PS4PasscodeBruteforcer() |
| 2142 | |
| 2143 | class BruteforceWorker(QObject): |
| 2144 | progress = pyqtSignal(str) |
| 2145 | tested = pyqtSignal(str) |
| 2146 | finished = pyqtSignal(str) |
| 2147 | |
| 2148 | def __init__(self, bruteforcer, input_file, output_dir, threads, seed_val): |
| 2149 | super().__init__() |
| 2150 | self._bf = bruteforcer |
| 2151 | self._in = input_file |
| 2152 | self._out = output_dir |
| 2153 | self._threads = threads |
| 2154 | self._seed = seed_val |
| 2155 | |
| 2156 | def run(self): |
| 2157 | try: |
| 2158 | result = self._bf.brute_force_passcode( |
| 2159 | self._in, |
| 2160 | self._out, |
| 2161 | progress_callback=self.progress.emit, |
| 2162 | manual_passcode=None, |
| 2163 | num_workers=self._threads, |
| 2164 | tested_callback=self.tested.emit, |
| 2165 | seed=self._seed |
| 2166 | ) |
| 2167 | self.finished.emit(result) |
| 2168 | except Exception as e: |
| 2169 | self.finished.emit(f"[-] Error: {str(e)}") |
| 2170 | |
| 2171 | # Parse seed (optional) |
| 2172 | seed_text = self.brute_seed_edit.text().strip() |
| 2173 | seed_val = None |
| 2174 | if seed_text: |
| 2175 | try: |
| 2176 | seed_val = int(seed_text) |
| 2177 | except ValueError: |
| 2178 | QMessageBox.warning(self, "Seed", "Seed must be an integer") |
| 2179 | self.brute_start_button.setEnabled(True) |
| 2180 | self.brute_stop_button.setEnabled(False) |
| 2181 | return |
| 2182 | |
| 2183 | self.brute_thread = QThread(self) |
| 2184 | self.brute_worker = BruteforceWorker(self.bruteforcer, self.package.original_file, output_dir, self.brute_threads_spin.value(), seed_val) |
| 2185 | self.brute_worker.moveToThread(self.brute_thread) |
| 2186 | self.brute_thread.started.connect(self.brute_worker.run) |
| 2187 | self.brute_worker.progress.connect(self.bruteforce_log.append) |
| 2188 | self.brute_worker.progress.connect(self.on_bruteforce_progress) |
| 2189 | self.brute_worker.tested.connect(self.on_tested_key) |
| 2190 | self.brute_worker.finished.connect(self.on_bruteforce_finished) |
| 2191 | self.brute_worker.finished.connect(self.brute_thread.quit) |
| 2192 | self.brute_thread.finished.connect(self.brute_thread.deleteLater) |
| 2193 | self.brute_thread.start() |
| 2194 | except Exception as e: |
| 2195 | QMessageBox.critical(self, "Error", f"Failed to start bruteforce: {str(e)}") |
| 2196 | |
| 2197 | def stop_bruteforce(self): |
| 2198 | try: |
| 2199 | if hasattr(self, 'bruteforcer') and self.bruteforcer: |
| 2200 | # Prefer a stop() method if available, else set internal flag |
| 2201 | if hasattr(self.bruteforcer, 'stop') and callable(self.bruteforcer.stop): |
| 2202 | self.bruteforcer.stop() |
| 2203 | else: |
| 2204 | setattr(self.bruteforcer, '_stop', True) |
| 2205 | self.brute_stop_button.setEnabled(False) |
| 2206 | except Exception as e: |
| 2207 | logging.error(f"Failed to stop bruteforce: {e}") |
| 2208 | |
| 2209 | def reset_bruteforce(self): |
| 2210 | """Stop any running bruteforce, delete saved state/success files, and reset UI.""" |
| 2211 | try: |
| 2212 | # 1) Stop current run if any |
| 2213 | self.stop_bruteforce() |
| 2214 | |
| 2215 | # 2) Determine current input file |
| 2216 | input_file = None |
| 2217 | try: |
| 2218 | if hasattr(self, 'package') and self.package and hasattr(self.package, 'original_file'): |
| 2219 | input_file = self.package.original_file |
| 2220 | except Exception: |
| 2221 | input_file = None |
| 2222 | if not input_file: |
| 2223 | # fallback from UI text |
| 2224 | input_file = self.pkg_entry.text().strip() |
| 2225 | |
| 2226 | # 3) Delete state and success files |
| 2227 | if input_file: |
| 2228 | state_path = f"{input_file}.brutestate.json" |
| 2229 | success_path = f"{input_file}.success" |
| 2230 | try: |
| 2231 | if os.path.exists(state_path): |
| 2232 | os.remove(state_path) |
| 2233 | self.bruteforce_log.append(f"[+] Removed state file: {state_path}") |
| 2234 | except Exception as e: |
| 2235 | self.bruteforce_log.append(f"[-] Could not remove state file: {e}") |
| 2236 | try: |
| 2237 | if os.path.exists(success_path): |
| 2238 | os.remove(success_path) |
| 2239 | self.bruteforce_log.append(f"[+] Removed success file: {success_path}") |
| 2240 | except Exception as e: |
| 2241 | self.bruteforce_log.append(f"[-] Could not remove success file: {e}") |
| 2242 | |
| 2243 | # 4) Reset UI elements |
| 2244 | self.bruteforce_log.clear() |
| 2245 | self.tested_keys_list.clear() |
| 2246 | self.tested_count_label.setText("Shown: 0 (max 1000)") |
| 2247 | self.brute_attempts_label.setText("Attempts: 0") |
| 2248 | self.brute_rate_label.setText("Rate: 0/s") |
| 2249 | self.brute_start_button.setEnabled(True) |
| 2250 | self.brute_stop_button.setEnabled(False) |
| 2251 | |
| 2252 | QMessageBox.information(self, "Reset", "Bruteforce state has been reset.") |
| 2253 | except Exception as e: |
| 2254 | logging.error(f"Failed to reset bruteforce: {e}") |
| 2255 | QMessageBox.critical(self, "Reset", f"Failed to reset: {e}") |
| 2256 | |
| 2257 | def on_tested_key(self, key: str): |
| 2258 | # Append with bounded size to avoid memory growth |
| 2259 | MAX_ITEMS = 1000 |
| 2260 | self.tested_keys_list.addItem(key) |
| 2261 | if self.tested_keys_list.count() > MAX_ITEMS: |
| 2262 | # Remove from top (oldest) |
| 2263 | item = self.tested_keys_list.takeItem(0) |
| 2264 | del item |
| 2265 | self.tested_count_label.setText(f"Shown: {self.tested_keys_list.count()} (max {MAX_ITEMS})") |
| 2266 | |
| 2267 | def on_bruteforce_finished(self, result: str): |
| 2268 | # Re-enable UI and show result |
| 2269 | self.brute_start_button.setEnabled(True) |
| 2270 | self.brute_stop_button.setEnabled(False) |
| 2271 | if result: |
| 2272 | self.bruteforce_log.append(result) |
| 2273 | if "successfully" in result.lower() or "[+]" in result: |
| 2274 | QMessageBox.information(self, "Success", result) |
| 2275 | elif result.lower().startswith("[-]"): |
| 2276 | # Show warning for negative outcome |
| 2277 | QMessageBox.warning(self, "Bruteforce", result) |
| 2278 | |
| 2279 | def on_bruteforce_progress(self, msg: str): |
| 2280 | # Parse attempts/rate lines like: "[~] Attempts: N | Rate: R/s" or with Threads |
| 2281 | try: |
| 2282 | m = re.search(r"Attempts:\s*(\d+).*Rate:\s*([0-9]+(?:\.[0-9]+)?)", msg) |
| 2283 | if m: |
| 2284 | self.brute_attempts_label.setText(f"Attempts: {m.group(1)}") |
| 2285 | self.brute_rate_label.setText(f"Rate: {m.group(2)}/s") |
| 2286 | except Exception: |
| 2287 | pass |
| 2288 | |
| 2289 | def show_about(self): |
| 2290 | """Show about dialog""" |
| 2291 | QMessageBox.about(self, |
| 2292 | "About PKG Tool Box", |
| 2293 | """<h3>PKG Tool Box v1.4.02</h3> |
| 2294 | <p>Created by SeregonWar</p> |
| 2295 | <p>A tool for managing PS3/PS4/PS5 PKG files.</p> |
| 2296 | <p><a href="https://github.com/seregonwar">GitHub</a> | |
| 2297 | <a href="https://ko-fi.com/seregon">Support on Ko-fi</a></p>""" |
| 2298 | ) |
| 2299 | |
| 2300 | def update_info(self, info_dict): |
| 2301 | """Update info tab with package information""" |
| 2302 | if hasattr(self.info_tab, 'update_info'): |
| 2303 | self.info_tab.update_info(info_dict) |
| 2304 | |
| 2305 | def update_pkg_entries(self, filename): |
| 2306 | """Update all PKG-related entries with the new filename""" |
| 2307 | self.pkg_entry.setText(filename) |
| 2308 | |
| 2309 | # Set default output directory based on PKG location |
| 2310 | output_dir = os.path.join(os.path.dirname(filename), "output") |
| 2311 | |
| 2312 | # Update entries in various tabs |
| 2313 | if hasattr(self.extract_tab, 'extract_out_entry'): |
| 2314 | self.extract_tab.extract_out_entry.setText(output_dir) |
| 2315 | if hasattr(self.bruteforce_tab, 'bruteforce_out_entry'): |
| 2316 | self.bruteforce_tab.bruteforce_out_entry.setText(output_dir) |
| 2317 | |
| 2318 | def setup_shortcuts(self): |
| 2319 | """Setup keyboard shortcuts""" |
| 2320 | shortcuts = { |
| 2321 | 'Ctrl+O': self.browse_pkg, |
| 2322 | 'Ctrl+E': lambda: self.tab_widget.setCurrentWidget(self.extract_tab), |
| 2323 | 'Ctrl+I': lambda: self.tab_widget.setCurrentWidget(self.info_tab), |
| 2324 | 'Ctrl+F': self.file_browser.file_search.setFocus, |
| 2325 | 'Ctrl+B': self.toggle_sidebar, # Toggle sidebar |
| 2326 | 'Ctrl+W': lambda: self.tab_widget.setCurrentWidget(self.wallpaper_viewer), |
| 2327 | 'Ctrl+T': self.show_theme_menu, # Open theme menu |
| 2328 | 'F5': self.refresh_all, |
| 2329 | 'F11': self.toggle_fullscreen |
| 2330 | } |
| 2331 | |
| 2332 | for key, func in shortcuts.items(): |
| 2333 | sc = QShortcut(QKeySequence(key), self) |
| 2334 | sc.activated.connect(func) |
| 2335 | |
| 2336 | # Alt+1..9 to jump to primary sections |
| 2337 | tab_widgets = [ |
| 2338 | self.info_tab, |
| 2339 | self.file_browser, |
| 2340 | self.wallpaper_viewer, |
| 2341 | self.extract_tab, |
| 2342 | self.inject_tab, |
| 2343 | self.modify_tab, |
| 2344 | self.trophy_tab, |
| 2345 | self.esmf_decrypter_tab, |
| 2346 | ] |
| 2347 | for i, widget in enumerate(tab_widgets, start=1): |
| 2348 | seq = QKeySequence(f"Alt+{i}") |
| 2349 | qs = QShortcut(seq, self) |
| 2350 | qs.activated.connect(lambda w=widget: self.tab_widget.setCurrentWidget(w)) |
| 2351 | |
| 2352 | def setup_search(self): |
| 2353 | """Setup global search""" |
| 2354 | search_widget = QWidget() |
| 2355 | search_layout = QHBoxLayout(search_widget) |
| 2356 | |
| 2357 | self.global_search = QLineEdit() |
| 2358 | self.global_search.setPlaceholderText("Search everywhere...") |
| 2359 | self.global_search.textChanged.connect(self.perform_global_search) |
| 2360 | |
| 2361 | search_layout.addWidget(self.global_search) |
| 2362 | |
| 2363 | # Aggiungi alla toolbar |
| 2364 | search_toolbar = QToolBar() |
| 2365 | search_toolbar.addWidget(search_widget) |
| 2366 | self.addToolBar(Qt.TopToolBarArea, search_toolbar) |
| 2367 | |
| 2368 | def perform_global_search(self, text): |
| 2369 | """Perform search across all tabs""" |
| 2370 | if not text: |
| 2371 | return |
| 2372 | |
| 2373 | results = [] |
| 2374 | |
| 2375 | # Cerca nei file |
| 2376 | if self.package: |
| 2377 | for file_info in self.package.files.values(): |
| 2378 | if text.lower() in file_info.get('name', '').lower(): |
| 2379 | results.append(('File', file_info['name'])) |
| 2380 | |
| 2381 | # Cerca nelle info |
| 2382 | for key, value in self.info_tree.items(): |
| 2383 | if text.lower() in str(value).lower(): |
| 2384 | results.append(('Info', f"{key}: {value}")) |
| 2385 | |
| 2386 | # Mostra risultati |
| 2387 | self.show_search_results(results) |
| 2388 | |
| 2389 | def show_error(self, title, message, details=None): |
| 2390 | """Show error dialog with details""" |
| 2391 | msg = QMessageBox(self) |
| 2392 | msg.setIcon(QMessageBox.Critical) |
| 2393 | msg.setWindowTitle(title) |
| 2394 | msg.setText(message) |
| 2395 | |
| 2396 | if details: |
| 2397 | msg.setDetailedText(details) |
| 2398 | |
| 2399 | msg.setStandardButtons(QMessageBox.Ok) |
| 2400 | return msg.exec_() |
| 2401 | |
| 2402 | def handle_error(self, error, operation="Operation"): |
| 2403 | """Handle errors with logging and user feedback""" |
| 2404 | error_msg = str(error) |
| 2405 | error_details = ''.join(traceback.format_exception(type(error), error, error.__traceback__)) |
| 2406 | |
| 2407 | Logger.log_error(f"{operation} failed: {error_msg}\n{error_details}") |
| 2408 | self.show_error( |
| 2409 | f"{operation} Failed", |
| 2410 | error_msg, |
| 2411 | error_details |
| 2412 | ) |
| 2413 | |
| 2414 | def setup_drag_drop(self): |
| 2415 | """Setup drag and drop between tabs""" |
| 2416 | self.setAcceptDrops(True) |
| 2417 | |
| 2418 | # Abilita drag and drop per i widget che lo supportano |
| 2419 | if hasattr(self.file_browser, 'file_tree'): |
| 2420 | self.file_browser.file_tree.setDragEnabled(True) |
| 2421 | self.file_browser.file_tree.setAcceptDrops(True) |
| 2422 | |
| 2423 | if hasattr(self.wallpaper_viewer, 'wallpaper_tree'): |
| 2424 | self.wallpaper_viewer.wallpaper_tree.setAcceptDrops(True) |
| 2425 | |
| 2426 | # Connetti i segnali se esistono |
| 2427 | if hasattr(self.file_browser, 'itemDropped'): |
| 2428 | self.file_browser.itemDropped.connect(self.handle_item_drop) |
| 2429 | if hasattr(self.wallpaper_viewer, 'itemDropped'): |
| 2430 | self.wallpaper_viewer.itemDropped.connect(self.handle_item_drop) |
| 2431 | |
| 2432 | def refresh_all(self): |
| 2433 | """Refresh all views and data""" |
| 2434 | try: |
| 2435 | if self.package: |
| 2436 | # Refresh file browser |
| 2437 | if hasattr(self, 'file_browser'): |
| 2438 | self.file_browser.load_files(self.package) |
| 2439 | |
| 2440 | # Refresh wallpaper viewer |
| 2441 | if hasattr(self, 'wallpaper_viewer'): |
| 2442 | self.wallpaper_viewer.load_wallpapers(self.package) |
| 2443 | |
| 2444 | # Refresh PKG icon and info |
| 2445 | self.load_pkg_icon() |
| 2446 | info_dict = self.package.get_info() |
| 2447 | self.update_info(info_dict) |
| 2448 | |
| 2449 | Logger.log_information("All views refreshed successfully") |
| 2450 | else: |
| 2451 | Logger.log_warning("No package loaded to refresh") |
| 2452 | |
| 2453 | except Exception as e: |
| 2454 | error_msg = f"Error refreshing views: {str(e)}" |
| 2455 | Logger.log_error(error_msg) |
| 2456 | QMessageBox.critical(self, "Error", error_msg) |
| 2457 | |
| 2458 | def toggle_fullscreen(self): |
| 2459 | """Toggle fullscreen mode""" |
| 2460 | if self.isFullScreen(): |
| 2461 | self.showNormal() |
| 2462 | else: |
| 2463 | self.showFullScreen() |
| 2464 | |
| 2465 | def browse_trophy(self): |
| 2466 | """Browse for trophy file""" |
| 2467 | filename, _ = QFileDialog.getOpenFileName( |
| 2468 | self, |
| 2469 | "Select Trophy file", |
| 2470 | "", |
| 2471 | "Trophy files (*.trp *.ucp);;TRP files (*.trp);;UCP files (*.ucp);;All files (*.*)" |
| 2472 | ) |
| 2473 | if filename: |
| 2474 | try: |
| 2475 | self.trophy_entry.setText(filename) |
| 2476 | |
| 2477 | # Carica le informazioni del trofeo |
| 2478 | trophy_reader = TRPReader(filename) |
| 2479 | |
| 2480 | # Mostra le informazioni nel text edit |
| 2481 | info_text = f""" |
| 2482 | Title: {trophy_reader._title if trophy_reader._title else 'N/A'} |
| 2483 | NP Communication ID: {trophy_reader._npcommid if trophy_reader._npcommid else 'N/A'} |
| 2484 | Number of Trophies: {len(trophy_reader._trophyList) if trophy_reader._trophyList else 0} |
| 2485 | File Type: {os.path.splitext(filename)[1].upper()[1:]} |
| 2486 | """ |
| 2487 | self.trophy_info.setText(info_text) |
| 2488 | |
| 2489 | # Carica i trofei nella tree view |
| 2490 | self.trophy_tree.clear() |
| 2491 | for trophy in trophy_reader._trophyList: |
| 2492 | item = QTreeWidgetItem(self.trophy_tree) |
| 2493 | item.setText(0, trophy.name) |
| 2494 | |
| 2495 | # Determina il tipo di trofeo dal nome del file |
| 2496 | if "TROP" in trophy.name.upper(): |
| 2497 | if "BRONZE" in trophy.name.upper(): |
| 2498 | trophy_type = "Bronze" |
| 2499 | elif "SILVER" in trophy.name.upper(): |
| 2500 | trophy_type = "Silver" |
| 2501 | elif "GOLD" in trophy.name.upper(): |
| 2502 | trophy_type = "Gold" |
| 2503 | elif "PLATINUM" in trophy.name.upper(): |
| 2504 | trophy_type = "Platinum" |
| 2505 | else: |
| 2506 | trophy_type = "Unknown" |
| 2507 | else: |
| 2508 | trophy_type = "Unknown" |
| 2509 | |
| 2510 | item.setText(1, trophy_type) # Tipo di trofeo |
| 2511 | item.setText(2, self.get_trophy_grade(trophy)) # Grado del trofeo |
| 2512 | item.setText(3, "No") # Hidden di default |
| 2513 | |
| 2514 | # Salva i dati del trofeo nell'item |
| 2515 | item.setData(0, Qt.UserRole, trophy) |
| 2516 | |
| 2517 | # Abilita/disabilita pulsanti in base al tipo di file |
| 2518 | is_trp = filename.lower().endswith('.trp') |
| 2519 | self.trophy_decrypt_button.setEnabled(is_trp) |
| 2520 | self.trophy_recompile_button.setEnabled(not is_trp) |
| 2521 | |
| 2522 | Logger.log_information(f"Trophy file loaded: {filename}") |
| 2523 | |
| 2524 | except Exception as e: |
| 2525 | error_msg = f"Error loading trophy file: {str(e)}" |
| 2526 | Logger.log_error(error_msg) |
| 2527 | QMessageBox.critical(self, "Error", error_msg) |
| 2528 | |
| 2529 | def display_selected_trophy(self, item, column): |
| 2530 | """Display selected trophy information""" |
| 2531 | try: |
| 2532 | trophy = item.data(0, Qt.UserRole) |
| 2533 | if not trophy: |
| 2534 | return |
| 2535 | |
| 2536 | # Aggiorna le informazioni del trofeo |
| 2537 | trophy_details = f""" |
| 2538 | Name: {trophy.name} |
| 2539 | Type: {self.get_trophy_type(trophy)} |
| 2540 | Grade: {self.get_trophy_grade(trophy)} |
| 2541 | Hidden: {'Yes' if hasattr(trophy, 'hidden') and trophy.hidden else 'No'} |
| 2542 | """ |
| 2543 | self.trophy_details.setText(trophy_details) |
| 2544 | |
| 2545 | # Carica l'immagine del trofeo se disponibile |
| 2546 | if trophy.name.upper().endswith('.PNG'): |
| 2547 | try: |
| 2548 | with open(self.trophy_entry.text(), 'rb') as f: |
| 2549 | f.seek(trophy.offset) |
| 2550 | image_data = f.read(trophy.size) |
| 2551 | pixmap = ImageUtils.create_thumbnail(image_data) |
| 2552 | self.trophy_image_viewer.setPixmap(pixmap) |
| 2553 | self.trophy_image_viewer.setAlignment(Qt.AlignCenter) |
| 2554 | except Exception as e: |
| 2555 | Logger.log_error(f"Error loading trophy image: {str(e)}") |
| 2556 | self.trophy_image_viewer.clear() |
| 2557 | else: |
| 2558 | self.trophy_image_viewer.clear() |
| 2559 | |
| 2560 | except Exception as e: |
| 2561 | Logger.log_error(f"Error displaying trophy: {str(e)}") |
| 2562 | self.trophy_details.clear() |
| 2563 | self.trophy_image_viewer.clear() |
| 2564 | |
| 2565 | def get_trophy_type(self, trophy): |
| 2566 | """Get trophy type based on filename""" |
| 2567 | name = trophy.name.upper() |
| 2568 | if "BRONZE" in name: |
| 2569 | return "Bronze" |
| 2570 | elif "SILVER" in name: |
| 2571 | return "Silver" |
| 2572 | elif "GOLD" in name: |
| 2573 | return "Gold" |
| 2574 | elif "PLATINUM" in name: |
| 2575 | return "Platinum" |
| 2576 | return "Unknown" |
| 2577 | |
| 2578 | def get_trophy_grade(self, trophy): |
| 2579 | """Get trophy grade based on filename""" |
| 2580 | name = trophy.name.upper() |
| 2581 | if "TROP" in name: |
| 2582 | if "BRONZE" in name: |
| 2583 | if "COMMON" in name: |
| 2584 | return "Common" |
| 2585 | elif "UNCOMMON" in name: |
| 2586 | return "Uncommon" |
| 2587 | elif "RARE" in name: |
| 2588 | return "Rare" |
| 2589 | elif "VERY_RARE" in name: |
| 2590 | return "Very Rare" |
| 2591 | return "Common" # Default per Bronze |
| 2592 | elif "SILVER" in name: |
| 2593 | if "COMMON" in name: |
| 2594 | return "Common" |
| 2595 | elif "UNCOMMON" in name: |
| 2596 | return "Uncommon" |
| 2597 | elif "RARE" in name: |
| 2598 | return "Rare" |
| 2599 | elif "VERY_RARE" in name: |
| 2600 | return "Very Rare" |
| 2601 | return "Uncommon" # Default per Silver |
| 2602 | elif "GOLD" in name: |
| 2603 | if "COMMON" in name: |
| 2604 | return "Common" |
| 2605 | elif "UNCOMMON" in name: |
| 2606 | return "Uncommon" |
| 2607 | elif "RARE" in name: |
| 2608 | return "Rare" |
| 2609 | elif "VERY_RARE" in name: |
| 2610 | return "Very Rare" |
| 2611 | return "Rare" # Default per Gold |
| 2612 | elif "PLATINUM" in name: |
| 2613 | if "COMMON" in name: |
| 2614 | return "Common" |
| 2615 | elif "UNCOMMON" in name: |
| 2616 | return "Uncommon" |
| 2617 | elif "RARE" in name: |
| 2618 | return "Rare" |
| 2619 | elif "VERY_RARE" in name: |
| 2620 | return "Very Rare" |
| 2621 | return "Very Rare" # Default per Platinum |
| 2622 | return "Unknown" |
| 2623 | |
| 2624 | def show_previous_trophy(self): |
| 2625 | """Show previous trophy in the list""" |
| 2626 | current_item = self.trophy_tree.currentItem() |
| 2627 | if current_item: |
| 2628 | current_index = self.trophy_tree.indexOfTopLevelItem(current_item) |
| 2629 | if current_index > 0: |
| 2630 | previous_item = self.trophy_tree.topLevelItem(current_index - 1) |
| 2631 | self.trophy_tree.setCurrentItem(previous_item) |
| 2632 | self.display_selected_trophy(previous_item, 0) |
| 2633 | |
| 2634 | def show_next_trophy(self): |
| 2635 | """Show next trophy in the list""" |
| 2636 | current_item = self.trophy_tree.currentItem() |
| 2637 | if current_item: |
| 2638 | current_index = self.trophy_tree.indexOfTopLevelItem(current_item) |
| 2639 | if current_index < self.trophy_tree.topLevelItemCount() - 1: |
| 2640 | next_item = self.trophy_tree.topLevelItem(current_index + 1) |
| 2641 | self.trophy_tree.setCurrentItem(next_item) |
| 2642 | self.display_selected_trophy(next_item, 0) |
| 2643 | |
| 2644 | def edit_trophy_info(self): |
| 2645 | """Edit selected trophy information""" |
| 2646 | selected_items = self.trophy_tree.selectedItems() |
| 2647 | if not selected_items: |
| 2648 | QMessageBox.warning(self, "Warning", "No trophy selected") |
| 2649 | return |
| 2650 | |
| 2651 | item = selected_items[0] |
| 2652 | trophy_data = item.data(0, Qt.UserRole) |
| 2653 | |
| 2654 | if not trophy_data: |
| 2655 | return |
| 2656 | |
| 2657 | try: |
| 2658 | # Mostra dialog per modificare le informazioni |
| 2659 | dialog = QDialog(self) |
| 2660 | dialog.setWindowTitle("Edit Trophy Info") |
| 2661 | layout = QVBoxLayout(dialog) |
| 2662 | |
| 2663 | # Form per le informazioni modificabili |
| 2664 | form_layout = QGridLayout() |
| 2665 | name_edit = QLineEdit(trophy_data.get('name', '')) |
| 2666 | desc_edit = QTextEdit(trophy_data.get('description', '')) |
| 2667 | type_combo = QComboBox() |
| 2668 | type_combo.addItems(['Bronze', 'Silver', 'Gold', 'Platinum']) |
| 2669 | type_combo.setCurrentText(trophy_data.get('type', 'Bronze')) |
| 2670 | hidden_check = QCheckBox("Hidden") |
| 2671 | hidden_check.setChecked(trophy_data.get('hidden', False)) |
| 2672 | |
| 2673 | form_layout.addWidget(QLabel("Name:"), 0, 0) |
| 2674 | form_layout.addWidget(name_edit, 0, 1) |
| 2675 | form_layout.addWidget(QLabel("Description:"), 1, 0) |
| 2676 | form_layout.addWidget(desc_edit, 1, 1) |
| 2677 | form_layout.addWidget(QLabel("Type:"), 2, 0) |
| 2678 | form_layout.addWidget(type_combo, 2, 1) |
| 2679 | form_layout.addWidget(hidden_check, 3, 1) |
| 2680 | |
| 2681 | layout.addLayout(form_layout) |
| 2682 | |
| 2683 | # Pulsanti |
| 2684 | buttons = QHBoxLayout() |
| 2685 | save_btn = QPushButton("Save") |
| 2686 | cancel_btn = QPushButton("Cancel") |
| 2687 | save_btn.clicked.connect(dialog.accept) |
| 2688 | cancel_btn.clicked.connect(dialog.reject) |
| 2689 | buttons.addWidget(save_btn) |
| 2690 | buttons.addWidget(cancel_btn) |
| 2691 | layout.addLayout(buttons) |
| 2692 | |
| 2693 | if dialog.exec_() == QDialog.Accepted: |
| 2694 | # Aggiorna i dati del trofeo |
| 2695 | trophy_data['name'] = name_edit.text() |
| 2696 | trophy_data['description'] = desc_edit.toPlainText() |
| 2697 | trophy_data['type'] = type_combo.currentText() |
| 2698 | trophy_data['hidden'] = hidden_check.isChecked() |
| 2699 | |
| 2700 | # Aggiorna la visualizzazione |
| 2701 | item.setText(0, trophy_data['name']) |
| 2702 | item.setText(1, trophy_data['type']) |
| 2703 | item.setText(2, trophy_data['grade']) |
| 2704 | item.setText(3, 'Yes' if trophy_data['hidden'] else 'No') |
| 2705 | |
| 2706 | self.display_selected_trophy(item, 0) |
| 2707 | |
| 2708 | except Exception as e: |
| 2709 | QMessageBox.critical(self, "Error", f"Failed to edit trophy: {str(e)}") |
| 2710 | |
| 2711 | def recompile_trp(self): |
| 2712 | """Recompile TRP file""" |
| 2713 | try: |
| 2714 | if not hasattr(self, 'trophy_files'): |
| 2715 | QMessageBox.warning(self, "Warning", "No trophy files loaded") |
| 2716 | return |
| 2717 | |
| 2718 | output_path, _ = QFileDialog.getSaveFileName( |
| 2719 | self, |
| 2720 | "Save TRP File", |
| 2721 | "", |
| 2722 | "TRP files (*.trp)" |
| 2723 | ) |
| 2724 | |
| 2725 | if not output_path: |
| 2726 | return |
| 2727 | |
| 2728 | creator = TRPCreator() |
| 2729 | creator.create(output_path, self.trophy_files) |
| 2730 | |
| 2731 | QMessageBox.information(self, "Success", "TRP file created successfully") |
| 2732 | |
| 2733 | except Exception as e: |
| 2734 | QMessageBox.critical(self, "Error", f"Failed to create TRP: {str(e)}") |
| 2735 | |
| 2736 | def decrypt_trophy(self): |
| 2737 | """Decrypt selected trophy file""" |
| 2738 | try: |
| 2739 | if not self.trophy_entry.text(): |
| 2740 | QMessageBox.warning(self, "Warning", "No trophy file selected") |
| 2741 | return |
| 2742 | |
| 2743 | output_dir = QFileDialog.getExistingDirectory( |
| 2744 | self, |
| 2745 | "Select Output Directory" |
| 2746 | ) |
| 2747 | |
| 2748 | if not output_dir: |
| 2749 | return |
| 2750 | |
| 2751 | decrypter = TRPReader() |
| 2752 | decrypter.decrypt_trp(self.trophy_entry.text(), output_dir) |
| 2753 | |
| 2754 | QMessageBox.information(self, "Success", "Trophy file decrypted successfully") |
| 2755 | |
| 2756 | except Exception as e: |
| 2757 | QMessageBox.critical(self, "Error", f"Failed to decrypt trophy: {str(e)}") |
| 2758 | |
| 2759 | def retranslate_ui(self): |
| 2760 | """Update UI text with current language""" |
| 2761 | # Update window title |
| 2762 | self.setWindowTitle(self.translator.translate("PKG Tool Box v1.4.0")) |
| 2763 | |
| 2764 | # Update menu items |
| 2765 | self.file_menu.setTitle(self.translator.translate("File")) |
| 2766 | self.tools_menu.setTitle(self.translator.translate("Tools")) |
| 2767 | self.view_menu.setTitle(self.translator.translate("View")) |
| 2768 | self.help_menu.setTitle(self.translator.translate("Help")) |
| 2769 | |
| 2770 | # Update actions |
| 2771 | self.open_action.setText(self.translator.translate("Open PKG")) |
| 2772 | self.exit_action.setText(self.translator.translate("Exit")) |
| 2773 | |
| 2774 | # Update tab names |
| 2775 | self.tab_widget.setTabText(0, self.translator.translate("Info")) |
| 2776 | self.tab_widget.setTabText(1, self.translator.translate("File Browser")) |
| 2777 | |
| 2778 | |
| 2779 | # Force update |
| 2780 | self.update() |
| 2781 | |
| 2782 | def should_skip_updates(self): |
| 2783 | """Check if user has chosen to skip updates""" |
| 2784 | try: |
| 2785 | config_file = os.path.join(os.path.expanduser("~"), ".pkgtoolbox", "update_preferences.json") |
| 2786 | if os.path.exists(config_file): |
| 2787 | with open(config_file, 'r') as f: |
| 2788 | prefs = json.load(f) |
| 2789 | return prefs.get("skip_updates", False) |
| 2790 | except: |
| 2791 | pass |
| 2792 | return False |
| 2793 | |
| 2794 | def show_update_dialog(self, version, download_url): |
| 2795 | """Show update dialog when new version is available""" |
| 2796 | dialog = UpdateDialog(version, download_url, self) |
| 2797 | dialog.exec_() |
| 2798 | |
| 2799 | def handle_update_error(self, error_msg): |
| 2800 | """Handle errors during update check""" |
| 2801 | Logger.log_error(f"Update check failed: {error_msg}") |