Seregon/PkgToolBox

Toolbox for analyzing and editing pkg application files for psp,ps3, ps4 and ps5, includes the most useful functions you might need.

Python/57.3 KB/No license
GraphicUserInterface/main_window.py
PkgToolBox / GraphicUserInterface / main_window.py
1import logging
2import sys
3import os
4import re
5from 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)
8from PyQt5.QtCore import Qt, QSize, QUrl, QObject, pyqtSignal, QThread
9from PyQt5.QtGui import QFont, QDesktopServices
10from PyQt5.QtWidgets import QStyle
11import struct
12from GraphicUserInterface.components import FileBrowser, WallpaperViewer
13from GraphicUserInterface.dialogs import SettingsDialog
14from GraphicUserInterface.utils import StyleManager, ImageUtils, FileUtils
15from GraphicUserInterface.widgets import ExtractTab, InfoTab, BruteforceTab
16from GraphicUserInterface.widgets.pfs_info_tab import PfsInfoTab
17from tools.PS5_Game_Info import PS5GameInfo
18from packages import PackagePS4, PackagePS5, PackagePS3
19from file_operations import extract_file, inject_file, modify_file_header
20from Utilities.Trophy import ESMFDecrypter, TRPCreator
21from tools.PS4_Passcode_Bruteforcer import PS4PasscodeBruteforcer
22import re
23from Utilities import Logger
24import json
25from PyQt5.QtWidgets import QTableWidgetItem
26from PyQt5.QtGui import QKeySequence
27import traceback
28from Utilities import Logger, SettingsManager, TRPReader
29from .locales.translator import Translator
30from GraphicUserInterface.utils.update_checker import UpdateChecker, UpdateDialog
31 
32class 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}")