Background scrape/eject/refresh, PS1 PIC0 art polish, top-right logo, and updated screenshot

This commit is contained in:
2026-09-09 21:25:34 -04:00
parent 8c5f9b6ea0
commit 6e06d1389c
3 changed files with 168 additions and 61 deletions
+30 -15
View File
@@ -937,25 +937,28 @@ def _make_pic0(title, description, esrb, QImage, QPainter, QPainterPath, QColor,
QRectF, Qt, _save):
W, H = 310, 180
img = QImage(W, H, QImage.Format_ARGB32)
img.fill(QColor(0, 0, 0, 0))
# Dark gradient backdrop.
# Dark gradient backdrop clipped to a rounded card (transparent corners).
grad = QLinearGradient(0, 0, 0, H)
grad.setColorAt(0.0, QColor(38, 38, 40))
grad.setColorAt(1.0, QColor(12, 12, 14))
card = QPainterPath()
card.addRoundedRect(QRectF(0, 0, W, H), 12, 12)
p = QPainter(img)
p.setRenderHint(QPainter.Antialiasing, True)
p.setRenderHint(QPainter.TextAntialiasing, True)
p.fillRect(0, 0, W, H, grad)
p.fillPath(card, grad)
# Rounded border.
# Rounded card border.
p.setPen(QPen(QColor(110, 110, 115), 1))
p.setBrush(Qt.NoBrush)
p.drawRoundedRect(QRectF(4, 4, W - 8, H - 8), 9, 9)
p.drawPath(card)
# PlayStation button glyphs, top-right.
_draw_ps_glyphs(p, W - 34, 12, 22, QColor(200, 200, 205), QRectF, QPointF,
QPolygonF, QPen, Qt)
_draw_ps_glyphs(p, W - 34, 12, 22, QRectF, QPointF, QPolygonF, QPen, Qt,
QColor)
# Title (bold, upper-left). Shrink the font until the whole title fits, so
# long names are shown in full instead of being cut with an ellipsis.
@@ -977,6 +980,10 @@ def _make_pic0(title, description, esrb, QImage, QPainter, QPainterPath, QColor,
p.drawText(QRectF(18, 16, title_width, 28), Qt.AlignLeft | Qt.AlignVCenter,
title_text)
# Thin divider under the title (like the original PSN info board).
p.setPen(QPen(QColor(255, 255, 255, 55), 1))
p.drawLine(QPointF(18, 46), QPointF(W - 18, 46))
# Description (wrapped to at most 3 lines, last line elided).
if description:
f_desc = QFont("DejaVu Sans")
@@ -985,7 +992,7 @@ def _make_pic0(title, description, esrb, QImage, QPainter, QPainterPath, QColor,
p.setPen(QColor(215, 215, 215))
fm = QFontMetrics(f_desc)
lines = _wrap_elided(fm, description.strip(), W - 36, 3, Qt)
y = 46
y = 52
for line in lines:
p.drawText(QRectF(18, y, W - 36, 16), Qt.AlignLeft | Qt.AlignVCenter, line)
y += 16
@@ -1027,23 +1034,31 @@ def _wrap_elided(fm, text, width, max_lines, Qt):
return lines[:max_lines]
def _draw_ps_glyphs(p, x, y, size, color, QRectF, QPointF, QPolygonF, QPen, Qt):
"""Draw the four PlayStation face-button glyphs in a 2x2 grid."""
def _draw_ps_glyphs(p, x, y, size, QRectF, QPointF, QPolygonF, QPen, Qt, QColor):
"""Draw the four PlayStation face-button glyphs in a 2x2 grid, using the
official button colours (green triangle, red circle, blue cross, pink square)."""
cell = size / 2.0
p.setBrush(Qt.NoBrush)
def _pen(color):
pen = QPen(color, max(1.0, size / 14.0))
pen.setCapStyle(Qt.RoundCap)
pen.setJoinStyle(Qt.RoundJoin)
p.setPen(pen)
p.setBrush(Qt.NoBrush)
# Triangle (top-left).
return pen
# Triangle (top-left, green).
p.setPen(_pen(QColor("#3F9B0F")))
tri = QPolygonF([QPointF(x, y + cell), QPointF(x + cell, y), QPointF(x + cell, y + cell)])
p.drawPolygon(tri)
# Circle (top-right).
# Circle (top-right, red).
p.setPen(_pen(QColor("#DE1C18")))
p.drawEllipse(QRectF(x + cell, y, cell, cell))
# Cross (bottom-left).
# Cross (bottom-left, blue).
p.setPen(_pen(QColor("#287FC1")))
p.drawLine(QPointF(x + 1, y + cell + 1), QPointF(x + cell - 1, y + size - 1))
p.drawLine(QPointF(x + cell - 1, y + cell + 1), QPointF(x + 1, y + size - 1))
# Square (bottom-right).
# Square (bottom-right, pink).
p.setPen(_pen(QColor("#C94E9B")))
p.drawRect(QRectF(x + cell, y + cell, cell, cell))
+136 -44
View File
@@ -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 ""
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 740 KiB

After

Width:  |  Height:  |  Size: 683 KiB