Background scrape/eject/refresh, PS1 PIC0 art polish, top-right logo, and updated screenshot
This commit is contained in:
+30
-15
@@ -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
@@ -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 ""
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 740 KiB After Width: | Height: | Size: 683 KiB |
Reference in New Issue
Block a user