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/components/file_browser.py
1from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QTreeWidget,
2 QLineEdit, QTreeWidgetItem, QMenu, QMessageBox, QFileDialog,
3 QDialog, QVBoxLayout, QTextEdit, QLabel, QPushButton, QProgressBar,
4 QSlider)
5from PyQt5.QtCore import Qt, QSize, QThread, pyqtSignal, QUrl
6from PyQt5.QtWidgets import QStyle, QSplitter, QTabWidget
7from PyQt5.QtGui import QFont, QIcon, QPixmap
8from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent
9from ..utils import FileUtils, ImageUtils
10import os
11import threading
12import queue
13import time
14 
15class FileLoadWorker(QThread):
16 progress = pyqtSignal(int)
17 finished = pyqtSignal()
18
19 def __init__(self, package, file_structure):
20 super().__init__()
21 self.package = package
22 self.file_structure = file_structure
23
24 def run(self):
25 total = len(self.package.files)
26 for i, (file_id, file_info) in enumerate(self.package.files.items()):
27 if not file_info.get("name"):
28 continue
29
30 file_path = file_info["name"]
31 path_parts = file_path.split('/')
32
33 current_dict = self.file_structure
34
35 # Build directory structure
36 current_path = ""
37 for part in path_parts[:-1]:
38 if part:
39 current_path = os.path.join(current_path, part) if current_path else part
40 if part not in current_dict:
41 current_dict[part] = {
42 "_info": {
43 "is_dir": True,
44 "path": current_path,
45 "size": 0,
46 "files": [],
47 "subdirs": []
48 }
49 }
50 current_dict = current_dict[part]
51
52 # Add file to structure
53 if path_parts[-1]:
54 current_dict[path_parts[-1]] = file_info
55 # Update parent directory info
56 parent_dict = self.file_structure
57 for part in path_parts[:-1]:
58 if part:
59 parent_dict[part]["_info"]["size"] += file_info["size"]
60 if path_parts[-1] not in parent_dict[part]["_info"]["files"]:
61 parent_dict[part]["_info"]["files"].append(path_parts[-1])
62 parent_dict = parent_dict[part]
63
64 self.progress.emit(int((i+1)/total * 100))
65
66 # Update subdirs info
67 def update_subdirs(structure):
68 for name, content in structure.items():
69 if isinstance(content, dict) and "_info" in content:
70 parent_subdirs = []
71 for key in content.keys():
72 if key != "_info" and isinstance(content[key], dict) and "_info" in content[key]:
73 parent_subdirs.append(key)
74 content["_info"]["subdirs"] = parent_subdirs
75 update_subdirs(content)
76
77 update_subdirs(self.file_structure)
78 self.finished.emit()
79 
80class FileBrowser(QWidget):
81 def __init__(self, parent=None):
82 super().__init__(parent)
83 self.parent = parent
84 self.file_queue = queue.Queue()
85 self.preview_cache = {}
86 self.media_player = QMediaPlayer()
87 self.setup_ui()
88 
89 def add_file_item(self, parent_item, name, file_info):
90 file_item = QTreeWidgetItem(parent_item)
91 file_item.setText(0, name)
92 file_item.setText(1, FileUtils.format_size(file_info['size']))
93 file_item.setText(2, FileUtils.get_file_type(os.path.splitext(name)[1]))
94 file_item.setIcon(0, FileUtils.get_file_icon(name))
95 file_item.setData(0, Qt.UserRole, file_info)
96 return file_item
97
98 def setup_ui(self):
99 main_layout = QVBoxLayout(self)
100
101 # Toolbar
102 toolbar = QHBoxLayout()
103
104 # Search bar with icon
105 search_layout = QHBoxLayout()
106 self.file_search = QLineEdit()
107 self.file_search.setPlaceholderText("🔍 Search files...")
108 self.file_search.textChanged.connect(self.filter_files)
109 self.file_search.setStyleSheet("""
110 QLineEdit {
111 padding: 8px 12px;
112 border: 2px solid #3498db;
113 border-radius: 18px;
114 font-size: 14px;
115 background: #f8f9fa;
116 }
117 QLineEdit:focus {
118 border-color: #2980b9;
119 background: white;
120 }
121 """)
122
123 # Action buttons
124 self.refresh_btn = QPushButton("Refresh")
125 self.refresh_btn.clicked.connect(self.refresh_files)
126 self.refresh_btn.setStyleSheet("""
127 QPushButton {
128 padding: 8px 15px;
129 border-radius: 15px;
130 background: #3498db;
131 color: white;
132 font-weight: bold;
133 }
134 QPushButton:hover {
135 background: #2980b9;
136 }
137 """)
138
139 # File tree
140 self.file_tree = QTreeWidget()
141 self.file_tree.setHeaderLabels(["Name", "Size", "Type"])
142 self.file_tree.setColumnWidth(0, 400)
143 self.file_tree.setColumnWidth(1, 100)
144 self.file_tree.setColumnWidth(2, 100)
145 self.file_tree.setAlternatingRowColors(True)
146 self.file_tree.setAnimated(True)
147 self.file_tree.setIndentation(20)
148 self.file_tree.setSortingEnabled(True)
149 self.file_tree.setStyleSheet("""
150 QTreeWidget {
151 border: 1px solid #bdc3c7;
152 border-radius: 5px;
153 background-color: white;
154 }
155 QTreeWidget::item:hover {
156 background-color: #e8f6ff;
157 }
158 QTreeWidget::item:selected {
159 background-color: #3498db;
160 color: white;
161 }
162 """)
163
164 self.expand_btn = QPushButton("Expand All")
165 self.expand_btn.clicked.connect(self.file_tree.expandAll)
166 self.expand_btn.setStyleSheet(self.refresh_btn.styleSheet())
167
168 self.collapse_btn = QPushButton("Collapse All")
169 self.collapse_btn.clicked.connect(self.file_tree.collapseAll)
170 self.collapse_btn.setStyleSheet(self.refresh_btn.styleSheet())
171
172 toolbar.addWidget(self.file_search)
173 toolbar.addWidget(self.refresh_btn)
174 toolbar.addWidget(self.expand_btn)
175 toolbar.addWidget(self.collapse_btn)
176
177 main_layout.addLayout(toolbar)
178
179 # Progress bar
180 self.progress_bar = QProgressBar()
181 self.progress_bar.setVisible(False)
182 self.progress_bar.setStyleSheet("""
183 QProgressBar {
184 border: 2px solid #bdc3c7;
185 border-radius: 5px;
186 text-align: center;
187 }
188 QProgressBar::chunk {
189 background-color: #3498db;
190 }
191 """)
192 main_layout.addWidget(self.progress_bar)
193
194 # Splitter for tree and preview
195 splitter = QSplitter(Qt.Horizontal)
196
197 self.file_tree.setContextMenuPolicy(Qt.CustomContextMenu)
198 self.file_tree.customContextMenuRequested.connect(self.show_context_menu)
199 self.file_tree.itemSelectionChanged.connect(self.on_selection_changed)
200 self.file_tree.itemDoubleClicked.connect(self.on_item_double_clicked)
201
202 # Preview panel
203 self.preview_tabs = QTabWidget()
204 self.preview_tabs.setStyleSheet("""
205 QTabWidget::pane {
206 border: 1px solid #bdc3c7;
207 border-radius: 5px;
208 }
209 QTabBar::tab {
210 padding: 8px 12px;
211 margin: 2px;
212 }
213 QTabBar::tab:selected {
214 background: #3498db;
215 color: white;
216 }
217 """)
218
219 # Add widgets to splitter
220 splitter.addWidget(self.file_tree)
221 splitter.addWidget(self.preview_tabs)
222 splitter.setStretchFactor(0, 2)
223 splitter.setStretchFactor(1, 1)
224
225 main_layout.addWidget(splitter)
226
227 def filter_files(self):
228 search_text = self.file_search.text().lower()
229
230 def filter_item(item):
231 should_show = not search_text or search_text in item.text(0).lower()
232
233 if item.childCount() > 0:
234 for i in range(item.childCount()):
235 child = item.child(i)
236 child_visible = filter_item(child)
237 should_show = should_show or child_visible
238
239 item.setHidden(not should_show)
240 return should_show
241
242 root = self.file_tree.invisibleRootItem()
243 for i in range(root.childCount()):
244 filter_item(root.child(i))
245 
246 def load_files(self, package):
247 """Load files from package into tree view"""
248 self.file_tree.clear()
249 self.preview_tabs.clear()
250
251 if not package:
252 return
253
254 self.progress_bar.setVisible(True)
255 file_structure = {}
256
257 # Create and start worker thread
258 self.worker = FileLoadWorker(package, file_structure)
259 self.worker.progress.connect(self.progress_bar.setValue)
260 self.worker.finished.connect(self.on_files_loaded)
261 self.worker.finished.connect(lambda: self.progress_bar.setVisible(False))
262 self.worker.start()
263
264 def on_files_loaded(self):
265 def add_items(parent_item, structure, path=""):
266 for name, content in sorted(structure.items()):
267 if isinstance(content, dict):
268 if "_info" in content: # Directory
269 folder_item = QTreeWidgetItem(parent_item)
270 folder_item.setText(0, name)
271 folder_item.setText(1, FileUtils.format_size(content["_info"]["size"]))
272 folder_item.setText(2, "Directory")
273 folder_item.setIcon(0, FileUtils.get_file_icon('Directory'))
274 folder_item.setData(0, Qt.UserRole, content["_info"])
275
276 current_path = os.path.join(path, name) if path else name
277 add_items(folder_item, content, current_path)
278 else: # File
279 self.add_file_item(parent_item, name, content)
280 
281 add_items(self.file_tree.invisibleRootItem(), self.worker.file_structure)
282 self.file_tree.expandAll()
283 
284 def refresh_files(self):
285 if self.parent and self.parent.package:
286 self.load_files(self.parent.package)
287 
288 def on_selection_changed(self):
289 selected_items = self.file_tree.selectedItems()
290 if not selected_items:
291 return
292
293 item = selected_items[0]
294 file_info = item.data(0, Qt.UserRole)
295
296 if not file_info:
297 return
298
299 self.update_preview(item, file_info)
300
301 def update_preview(self, item, file_info):
302 self.preview_tabs.clear()
303
304 try:
305 # Info tab
306 info_widget = QWidget()
307 info_layout = QVBoxLayout(info_widget)
308 info_text = QTextEdit()
309 info_text.setReadOnly(True)
310
311 if file_info.get("is_dir"):
312 info_text.setPlainText(f"""
313 Directory Name: {item.text(0)}
314 Total Size: {FileUtils.format_size(file_info['size'])}
315 Files: {len(file_info['files'])}
316 Subdirectories: {len(file_info['subdirs'])}
317 Path: {file_info['path']}
318 """)
319 else:
320 data = self.parent.package.read_file(file_info['id'])
321 info_text.setPlainText(f"""
322 File Name: {item.text(0)}
323 Size: {FileUtils.format_size(file_info['size'])}
324 Type: {FileUtils.get_file_type(os.path.splitext(item.text(0))[1])}
325 Path: {file_info['name']}
326 """)
327
328 # Content preview based on file type
329 if FileUtils.is_text_file(item.text(0)):
330 text_content = data.decode('utf-8', errors='replace')
331 text_widget = QTextEdit()
332 text_widget.setReadOnly(True)
333 text_widget.setPlainText(text_content)
334 self.preview_tabs.addTab(text_widget, "Text View")
335
336 elif FileUtils.get_file_type(os.path.splitext(item.text(0))[1]) == 'Image':
337 pixmap = ImageUtils.create_thumbnail(data)
338 image_label = QLabel()
339 image_label.setPixmap(pixmap)
340 image_label.setAlignment(Qt.AlignCenter)
341 self.preview_tabs.addTab(image_label, "Image Preview")
342
343 elif FileUtils.get_file_type(os.path.splitext(item.text(0))[1]) == 'Audio':
344 # Audio player widget
345 audio_widget = QWidget()
346 audio_layout = QVBoxLayout(audio_widget)
347
348 # Create temporary file for audio playback
349 temp_file = os.path.join(os.path.dirname(__file__), "temp_audio")
350 with open(temp_file, "wb") as f:
351 f.write(data)
352
353 # Set up media player
354 self.media_player.setMedia(QMediaContent(QUrl.fromLocalFile(temp_file)))
355
356 # Add controls
357 play_btn = QPushButton("Play/Pause")
358 play_btn.clicked.connect(self.toggle_playback)
359
360 # Add slider for seeking
361 seek_slider = QSlider(Qt.Horizontal)
362 seek_slider.setRange(0, self.media_player.duration())
363 seek_slider.sliderMoved.connect(self.media_player.setPosition)
364
365 audio_layout.addWidget(play_btn)
366 audio_layout.addWidget(seek_slider)
367
368 self.preview_tabs.addTab(audio_widget, "Audio Player")
369
370 # Hex view for files
371 hex_widget = QTextEdit()
372 hex_widget.setReadOnly(True)
373 hex_widget.setFont(QFont("Courier"))
374 hex_view = ' '.join([f'{b:02X}' for b in data])
375 hex_widget.setPlainText(hex_view)
376 self.preview_tabs.addTab(hex_widget, "Hex View")
377
378 info_layout.addWidget(info_text)
379 self.preview_tabs.addTab(info_widget, "Info")
380
381 except Exception as e:
382 error_widget = QLabel(f"Error loading preview: {str(e)}")
383 self.preview_tabs.addTab(error_widget, "Error")
384 
385 def toggle_playback(self):
386 if self.media_player.state() == QMediaPlayer.PlayingState:
387 self.media_player.pause()
388 else:
389 self.media_player.play()
390 
391 def show_context_menu(self, position):
392 menu = QMenu()
393 menu.setStyleSheet("""
394 QMenu {
395 background-color: white;
396 border: 1px solid #bdc3c7;
397 }
398 QMenu::item {
399 padding: 5px 20px;
400 }
401 QMenu::item:selected {
402 background-color: #3498db;
403 color: white;
404 }
405 """)
406
407 extract_action = menu.addAction(QIcon.fromTheme("document-save"), "Extract")
408 hex_view_action = menu.addAction(QIcon.fromTheme("text-x-hex"), "View as Hex")
409 text_view_action = menu.addAction(QIcon.fromTheme("text-plain"), "View as Text")
410
411 extract_action.triggered.connect(self.extract_selected_file)
412 hex_view_action.triggered.connect(self.view_file_as_hex)
413 text_view_action.triggered.connect(self.view_file_as_text)
414
415 menu.exec_(self.file_tree.viewport().mapToGlobal(position))
416 
417 def extract_selected_file(self):
418 selected_items = self.file_tree.selectedItems()
419 if not selected_items:
420 QMessageBox.warning(self.parent, "Warning", "No file selected")
421 return
422
423 item = selected_items[0]
424 file_info = item.data(0, Qt.UserRole)
425
426 if not file_info:
427 return
428
429 output_path, _ = QFileDialog.getSaveFileName(
430 self.parent,
431 "Save File",
432 item.text(0)
433 )
434
435 if not output_path:
436 return
437
438 try:
439 data = self.parent.package.read_file(file_info['id'])
440 with open(output_path, 'wb') as f:
441 f.write(data)
442 QMessageBox.information(self.parent, "Success", f"File extracted to: {output_path}")
443
444 except Exception as e:
445 QMessageBox.critical(self.parent, "Error", f"Error extracting file: {str(e)}")
446 
447 def view_file_as_hex(self):
448 selected_items = self.file_tree.selectedItems()
449 if not selected_items:
450 QMessageBox.warning(self.parent, "Warning", "No file selected")
451 return
452
453 item = selected_items[0]
454 file_info = item.data(0, Qt.UserRole)
455
456 if not file_info:
457 return
458
459 try:
460 data = self.parent.package.read_file(file_info['id'])
461 hex_view = ' '.join([f'{b:02X}' for b in data])
462
463 dialog = QDialog(self.parent)
464 dialog.setWindowTitle(f"Hex View - {item.text(0)}")
465 dialog.resize(800, 600)
466
467 layout = QVBoxLayout(dialog)
468 text_edit = QTextEdit()
469 text_edit.setReadOnly(True)
470 text_edit.setFont(QFont("Courier", 10))
471 text_edit.setPlainText(hex_view)
472 text_edit.setStyleSheet("background-color: #f8f9fa;")
473
474 layout.addWidget(text_edit)
475 dialog.exec_()
476
477 except Exception as e:
478 QMessageBox.critical(self.parent, "Error", f"Error viewing file: {str(e)}")
479 
480 def view_file_as_text(self):
481 selected_items = self.file_tree.selectedItems()
482 if not selected_items:
483 QMessageBox.warning(self.parent, "Warning", "No file selected")
484 return
485
486 item = selected_items[0]
487 file_info = item.data(0, Qt.UserRole)
488
489 if not file_info:
490 return
491
492 if not FileUtils.is_text_file(item.text(0)):
493 QMessageBox.warning(self.parent, "Warning", "Selected file is not a text file")
494 return
495
496 try:
497 data = self.parent.package.read_file(file_info['id'])
498 text_content = data.decode('utf-8', errors='replace')
499
500 dialog = QDialog(self.parent)
501 dialog.setWindowTitle(f"Text View - {item.text(0)}")
502 dialog.resize(800, 600)
503
504 layout = QVBoxLayout(dialog)
505 text_edit = QTextEdit()
506 text_edit.setReadOnly(True)
507 text_edit.setPlainText(text_content)
508 text_edit.setStyleSheet("background-color: white;")
509
510 layout.addWidget(text_edit)
511 dialog.exec_()
512
513 except Exception as e:
514 QMessageBox.critical(self.parent, "Error", f"Error viewing file: {str(e)}")
515 
516 def on_item_double_clicked(self, item, column):
517 file_info = item.data(0, Qt.UserRole)
518 if not file_info:
519 return
520
521 if FileUtils.is_text_file(item.text(0)):
522 self.view_file_as_text()
523 elif FileUtils.get_file_type(os.path.splitext(item.text(0))[1]) == 'Image':
524 try:
525 data = self.parent.package.read_file(file_info['id'])
526 pixmap = ImageUtils.create_thumbnail(data)
527
528 dialog = QDialog(self.parent)
529 dialog.setWindowTitle(f"Preview - {item.text(0)}")
530 dialog.resize(800, 600)
531
532 layout = QVBoxLayout(dialog)
533 label = QLabel()
534 label.setPixmap(pixmap.scaled(
535 dialog.size(),
536 Qt.KeepAspectRatio,
537 Qt.SmoothTransformation
538 ))
539 label.setAlignment(Qt.AlignCenter)
540
541 layout.addWidget(label)
542 dialog.exec_()
543
544 except Exception as e:
545 QMessageBox.critical(self.parent, "Error", f"Error showing preview: {str(e)}")
546 else:
547 self.view_file_as_hex()
548 
549 def clear(self):
550 """Clear the file browser"""
551 self.file_tree.clear()
552 self.file_search.clear()
553 self.preview_tabs.clear()
554 self.preview_cache.clear()
555