Background scrape/eject/refresh, PS1 PIC0 art polish, top-right logo, and updated screenshot
This commit is contained in:
+136
-44
@@ -17,7 +17,7 @@ from PySide6.QtWidgets import (
|
||||
QAbstractItemView, QApplication, QCheckBox, QComboBox, QDialog, QDialogButtonBox,
|
||||
QFileDialog, QFormLayout, QGridLayout, QHBoxLayout, QHeaderView, QInputDialog, QLabel, QLineEdit,
|
||||
QListWidget, QListWidgetItem, QMainWindow, QMenu, QMessageBox, QPlainTextEdit,
|
||||
QProgressBar, QProgressDialog, QPushButton, QScrollArea, QSpinBox, QSplitter, QStackedWidget,
|
||||
QProgressBar, QPushButton, QScrollArea, QSpinBox, QSplitter, QStackedWidget,
|
||||
QTabWidget, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
@@ -1663,6 +1663,8 @@ class StorePage(QWidget):
|
||||
"""Browse the Redump 7z store and queue games for download."""
|
||||
|
||||
download_requested = Signal(list) # list of store item dicts
|
||||
scrape_status = Signal(str) # progress text for the window status bar
|
||||
scrape_complete = Signal() # fired when a background scrape finishes
|
||||
ICON_W, ICON_H = 150, 220
|
||||
GRID_W, GRID_H = 182, 262
|
||||
|
||||
@@ -1692,7 +1694,6 @@ class StorePage(QWidget):
|
||||
self._dirty = False
|
||||
self._fill_timer = None
|
||||
self._scraping = False
|
||||
self._scrape_dialog = None
|
||||
self._scrape_worker = None
|
||||
self._scrape_thread = None
|
||||
|
||||
@@ -2392,12 +2393,8 @@ class StorePage(QWidget):
|
||||
QMessageBox.information(self, "Nothing to scrape", "The store is empty.")
|
||||
return
|
||||
self._scraping = True
|
||||
dlg = QProgressDialog("Fetching metadata and covers for every game…", "Cancel", 0, len(tasks), self)
|
||||
dlg.setWindowTitle("Scrape store")
|
||||
dlg.setWindowModality(Qt.WindowModal)
|
||||
dlg.setMinimumDuration(0)
|
||||
dlg.setValue(0)
|
||||
self._scrape_dialog = dlg
|
||||
self.scrape_btn.setEnabled(False)
|
||||
self.scrape_status.emit("Scraping store…")
|
||||
|
||||
thread = QThread(self)
|
||||
worker = StoreScrapeAllWorker(tasks)
|
||||
@@ -2405,7 +2402,6 @@ class StorePage(QWidget):
|
||||
thread.started.connect(worker.run)
|
||||
worker.progress.connect(self._on_scrape_progress)
|
||||
worker.finished.connect(thread.quit)
|
||||
dlg.canceled.connect(self._on_scrape_cancelled)
|
||||
worker.finished.connect(self._on_scrape_done)
|
||||
self._scrape_thread = thread
|
||||
self._scrape_worker = worker
|
||||
@@ -2413,31 +2409,12 @@ class StorePage(QWidget):
|
||||
self._workers.append(worker)
|
||||
thread.start()
|
||||
|
||||
def _on_scrape_cancelled(self):
|
||||
worker = self._scrape_worker
|
||||
if worker is not None:
|
||||
worker.cancel()
|
||||
dlg = self._scrape_dialog
|
||||
if dlg is not None:
|
||||
dlg.close()
|
||||
# Drop the dialog reference so queued progress signals can't re-show it
|
||||
# (QProgressDialog re-appears on setValue while a value is pending).
|
||||
# Keep _scraping True until the worker actually finishes (_on_scrape_done)
|
||||
# so a re-click doesn't spawn a second scrape while this one winds down.
|
||||
self._scrape_dialog = None
|
||||
|
||||
def _on_scrape_progress(self, done, total):
|
||||
dlg = self._scrape_dialog
|
||||
if dlg is not None:
|
||||
dlg.setLabelText(f"Fetching metadata + covers… {done}/{total}")
|
||||
dlg.setValue(done)
|
||||
self.scrape_status.emit(f"Scraping store… {done}/{total}")
|
||||
|
||||
def _on_scrape_done(self):
|
||||
self._scraping = False
|
||||
dlg = self._scrape_dialog
|
||||
if dlg is not None:
|
||||
dlg.close()
|
||||
self._scrape_dialog = None
|
||||
self.scrape_btn.setEnabled(True)
|
||||
thread = self._scrape_thread
|
||||
worker = self._scrape_worker
|
||||
if thread is not None and thread in self._threads:
|
||||
@@ -2447,7 +2424,9 @@ class StorePage(QWidget):
|
||||
self._scrape_thread = None
|
||||
self._scrape_worker = None
|
||||
if not self._closing:
|
||||
self.scrape_status.emit("Store scrape complete")
|
||||
self._apply_filter()
|
||||
self.scrape_complete.emit()
|
||||
|
||||
def shutdown(self):
|
||||
self._closing = True
|
||||
@@ -2457,9 +2436,6 @@ class StorePage(QWidget):
|
||||
loader.cancel()
|
||||
if self._scrape_worker is not None:
|
||||
self._scrape_worker.cancel()
|
||||
dlg = self._scrape_dialog
|
||||
if dlg is not None:
|
||||
dlg.close()
|
||||
for t in list(self._threads):
|
||||
try:
|
||||
if t.isRunning():
|
||||
@@ -2649,6 +2625,61 @@ class PspInventoryWorker(QObject):
|
||||
self.finished.emit()
|
||||
|
||||
|
||||
class PspEjectWorker(QObject):
|
||||
"""Unmount a PSP off the GUI thread (udisksctl can block for seconds)."""
|
||||
result = Signal(bool, str) # ok, message
|
||||
finished = Signal()
|
||||
|
||||
def __init__(self, root):
|
||||
super().__init__()
|
||||
self.root = root
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
ok, msg = psp.eject(self.root)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
ok, msg = False, f"{type(exc).__name__}: {exc}"
|
||||
self.result.emit(ok, msg)
|
||||
self.finished.emit()
|
||||
|
||||
|
||||
class PspMountsWorker(QObject):
|
||||
"""Detect mounted PSP sticks + UMD volumes off the GUI thread."""
|
||||
result = Signal(int, list, list) # gen, mounts, umd_mounts
|
||||
finished = Signal()
|
||||
|
||||
def __init__(self, gen):
|
||||
super().__init__()
|
||||
self.gen = gen
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
mounts = psp.detect_mounts()
|
||||
umd = psp.umd_mounts()
|
||||
except Exception: # noqa: BLE001
|
||||
mounts, umd = [], []
|
||||
self.result.emit(self.gen, mounts, umd)
|
||||
self.finished.emit()
|
||||
|
||||
|
||||
class PspUmdScanWorker(QObject):
|
||||
"""Detect mounted UMD volumes off the GUI thread."""
|
||||
result = Signal(int, list) # gen, umd_mounts
|
||||
finished = Signal()
|
||||
|
||||
def __init__(self, gen):
|
||||
super().__init__()
|
||||
self.gen = gen
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
umd = psp.umd_mounts()
|
||||
except Exception: # noqa: BLE001
|
||||
umd = []
|
||||
self.result.emit(self.gen, umd)
|
||||
self.finished.emit()
|
||||
|
||||
|
||||
class PspSyncPage(QWidget):
|
||||
"""Show what is installed on a connected PSP and read a mounted UMD."""
|
||||
|
||||
@@ -2666,6 +2697,7 @@ class PspSyncPage(QWidget):
|
||||
self._lib_path_name = {}
|
||||
self._gen = 0
|
||||
self._umd_gen = 0
|
||||
self._mounts_gen = 0
|
||||
self._umd_iso = None
|
||||
self._umd_serial = None
|
||||
self._umd_kind = None
|
||||
@@ -2686,10 +2718,10 @@ class PspSyncPage(QWidget):
|
||||
refresh_btn = QPushButton("Refresh")
|
||||
refresh_btn.clicked.connect(self.refresh)
|
||||
device_row.addWidget(refresh_btn)
|
||||
eject_btn = QPushButton("Eject")
|
||||
eject_btn.setToolTip("Unmount the PSP so it is safe to unplug")
|
||||
eject_btn.clicked.connect(self._eject)
|
||||
device_row.addWidget(eject_btn)
|
||||
self.eject_btn = QPushButton("Eject")
|
||||
self.eject_btn.setToolTip("Unmount the PSP so it is safe to unplug")
|
||||
self.eject_btn.clicked.connect(self._eject)
|
||||
device_row.addWidget(self.eject_btn)
|
||||
layout.addLayout(device_row)
|
||||
|
||||
self.status = QLabel("No PSP connected")
|
||||
@@ -2846,10 +2878,25 @@ class PspSyncPage(QWidget):
|
||||
layout.addWidget(self.umd_table, 1)
|
||||
return page
|
||||
|
||||
def refresh_umd(self):
|
||||
def refresh_umd(self, umd_mounts=None):
|
||||
self._umd_gen += 1
|
||||
gen = self._umd_gen
|
||||
mounts = psp.umd_mounts()
|
||||
if umd_mounts is None:
|
||||
thread = QThread(self)
|
||||
worker = PspUmdScanWorker(gen)
|
||||
worker.moveToThread(thread)
|
||||
thread.started.connect(worker.run)
|
||||
worker.result.connect(self._on_umd_mounts)
|
||||
worker.finished.connect(thread.quit)
|
||||
self._threads.append(thread)
|
||||
self._workers.append(worker)
|
||||
thread.start()
|
||||
return
|
||||
self._on_umd_mounts(gen, umd_mounts)
|
||||
|
||||
def _on_umd_mounts(self, gen, mounts):
|
||||
if gen != self._umd_gen:
|
||||
return
|
||||
log.info("umd: %d mount(s) detected", len(mounts))
|
||||
if not mounts:
|
||||
self._clear_umd()
|
||||
@@ -3006,10 +3053,24 @@ class PspSyncPage(QWidget):
|
||||
self.install_requested.emit(items)
|
||||
|
||||
def refresh(self):
|
||||
current = self.device_combo.currentText()
|
||||
mounts = psp.detect_mounts()
|
||||
umd = psp.umd_mounts()
|
||||
self._mounts_gen += 1
|
||||
gen = self._mounts_gen
|
||||
log.info("psp sync: detecting mounts…")
|
||||
thread = QThread(self)
|
||||
worker = PspMountsWorker(gen)
|
||||
worker.moveToThread(thread)
|
||||
thread.started.connect(worker.run)
|
||||
worker.result.connect(self._on_mounts)
|
||||
worker.finished.connect(thread.quit)
|
||||
self._threads.append(thread)
|
||||
self._workers.append(worker)
|
||||
thread.start()
|
||||
|
||||
def _on_mounts(self, gen, mounts, umd):
|
||||
if gen != self._mounts_gen:
|
||||
return
|
||||
log.info("psp sync: %d mount(s), %d umd volume(s) detected", len(mounts), len(umd))
|
||||
current = self.device_combo.currentText()
|
||||
self.device_combo.blockSignals(True)
|
||||
self.device_combo.clear()
|
||||
self.device_combo.addItem("")
|
||||
@@ -3024,7 +3085,7 @@ class PspSyncPage(QWidget):
|
||||
self.device_combo.blockSignals(False)
|
||||
self._root = self._selected_root()
|
||||
self.refresh_inventory()
|
||||
self.refresh_umd()
|
||||
self.refresh_umd(umd)
|
||||
self.device_changed.emit(self._root)
|
||||
|
||||
def _selected_root(self):
|
||||
@@ -3249,7 +3310,21 @@ class PspSyncPage(QWidget):
|
||||
if not self._root:
|
||||
QMessageBox.information(self, "Eject", "No PSP selected.")
|
||||
return
|
||||
ok, msg = psp.eject(self._root)
|
||||
log.info("ejecting %s…", self._root)
|
||||
self.status.setText("Ejecting…")
|
||||
self.eject_btn.setEnabled(False)
|
||||
thread = QThread(self)
|
||||
worker = PspEjectWorker(self._root)
|
||||
worker.moveToThread(thread)
|
||||
thread.started.connect(worker.run)
|
||||
worker.result.connect(self._on_eject_result)
|
||||
worker.finished.connect(thread.quit)
|
||||
self._threads.append(thread)
|
||||
self._workers.append(worker)
|
||||
thread.start()
|
||||
|
||||
def _on_eject_result(self, ok, msg):
|
||||
self.eject_btn.setEnabled(True)
|
||||
log.info("eject %s -> %s", self._root, "ok" if ok else f"failed: {msg}")
|
||||
self.refresh()
|
||||
if ok:
|
||||
@@ -4095,6 +4170,18 @@ class MainWindow(QMainWindow):
|
||||
self.tabs = QTabWidget()
|
||||
self.tabs.setStyleSheet("QTabWidget::pane { border: 0px; padding: 10px 0 0 0; }")
|
||||
layout.addWidget(self.tabs)
|
||||
|
||||
# App logo in the top-right corner of the tab bar.
|
||||
logo = QLabel()
|
||||
logo.setStyleSheet("padding-right: 10px;")
|
||||
icon_path = config.PROJECT_ROOT / "img" / "app-icon.png"
|
||||
if icon_path.is_file():
|
||||
pm = QPixmap(str(icon_path))
|
||||
pm = pm.scaled(28, 28, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
logo.setPixmap(pm)
|
||||
logo.setToolTip("PSP Preservation Tool")
|
||||
self.tabs.setCornerWidget(logo, Qt.TopRightCorner)
|
||||
|
||||
self.queue = queue.TransferQueue(self)
|
||||
self.queue.job_finished.connect(self._on_queue_job_done)
|
||||
self.tabs.addTab(self._build_library_tab(), "Library")
|
||||
@@ -4103,6 +4190,8 @@ class MainWindow(QMainWindow):
|
||||
self._store_archive_id, self._store_archive_sig, self._store_archive_user,
|
||||
self._nps_dir, self._pkg2zip)
|
||||
self.store_page.download_requested.connect(self._on_store_download)
|
||||
self.store_page.scrape_status.connect(self._on_store_scrape_status)
|
||||
self.store_page.scrape_complete.connect(self.load_library)
|
||||
self.tabs.addTab(self.store_page, "Store")
|
||||
self.queue_page = QueuePage()
|
||||
self.queue_page.set_queue(self.queue)
|
||||
@@ -4619,6 +4708,9 @@ class MainWindow(QMainWindow):
|
||||
self.queue.enqueue(src, dest_dir, it, store.display_name(it["name"]), kind=kind)
|
||||
self.tabs.setCurrentWidget(self.queue_page)
|
||||
|
||||
def _on_store_scrape_status(self, msg):
|
||||
self.statusBar().showMessage(msg)
|
||||
|
||||
# -- PSP sync ---------------------------------------------------------- #
|
||||
def _on_psp_device_changed(self, root):
|
||||
self._psp_root = root or ""
|
||||
|
||||
Reference in New Issue
Block a user