saves: bigger icons, per-row checkboxes, delete + export

- Save icon bumped to 56x40.
- Added a checkbox column for multi-select and a 'Delete selected' button.
- Right-click context menu on a row offers Export (copy the save folder to a
  chosen dir) and Delete (remove from all locations, with confirmation).
This commit is contained in:
2026-09-07 21:48:41 -04:00
parent de931f422a
commit 0ef7d05462
+121 -9
View File
@@ -3366,22 +3366,32 @@ class SavesPage(QWidget):
self.sync_btn = QPushButton("Sync now") self.sync_btn = QPushButton("Sync now")
self.sync_btn.clicked.connect(lambda: self.sync_now(True)) self.sync_btn.clicked.connect(lambda: self.sync_now(True))
bar.addWidget(self.sync_btn) bar.addWidget(self.sync_btn)
self.delete_btn = QPushButton("Delete selected")
self.delete_btn.setEnabled(False)
self.delete_btn.setStyleSheet("color: #e06c6c;")
self.delete_btn.clicked.connect(self._delete_checked)
bar.addWidget(self.delete_btn)
bar.addStretch(1) bar.addStretch(1)
layout.addLayout(bar) layout.addLayout(bar)
self.table = QTableWidget(0, 5) self.table = QTableWidget(0, 6)
self.table.setHorizontalHeaderLabels(["Save", "ID", "PSP", "PPSSPP", "Cloud"]) self.table.setHorizontalHeaderLabels(["", "Save", "ID", "PSP", "PPSSPP", "Cloud"])
self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
self.table.verticalHeader().setVisible(False) self.table.verticalHeader().setVisible(False)
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers) self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.table.setSelectionBehavior(QAbstractItemView.SelectRows) self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.table.setSortingEnabled(True) self.table.setSortingEnabled(True)
self.table.setIconSize(QSize(40, 32)) self.table.setIconSize(QSize(56, 40))
self.table.horizontalHeader().setSortIndicator(0, Qt.AscendingOrder) self.table.setContextMenuPolicy(Qt.CustomContextMenu)
self.table.customContextMenuRequested.connect(self._on_context_menu)
self.table.itemChanged.connect(self._on_item_changed)
self.table.horizontalHeader().setSortIndicator(1, Qt.AscendingOrder)
layout.addWidget(self.table) layout.addWidget(self.table)
self.status = QLabel("") self.status = QLabel("")
layout.addWidget(self.status) layout.addWidget(self.status)
self._data = {}
self._populating = False
def _save_icon(self, folder): def _save_icon(self, folder):
"""Return a small QIcon from a save folder's ICON0.PNG, or None.""" """Return a small QIcon from a save folder's ICON0.PNG, or None."""
@@ -3393,7 +3403,7 @@ class SavesPage(QWidget):
pm = QPixmap(str(p)) pm = QPixmap(str(p))
if pm.isNull(): if pm.isNull():
return None return None
return QIcon(_fit_pixmap(pm, 40, 32)) return QIcon(_fit_pixmap(pm, 56, 40))
def set_locations(self, psp_dir, ppsspp_dir, cloud_dir): def set_locations(self, psp_dir, ppsspp_dir, cloud_dir):
self._psp_dir = psp_dir or "" self._psp_dir = psp_dir or ""
@@ -3404,19 +3414,119 @@ class SavesPage(QWidget):
def _locations(self): def _locations(self):
return {"psp": self._psp_dir, "ppsspp": self._ppsspp_dir, "cloud": self._cloud_dir} return {"psp": self._psp_dir, "ppsspp": self._ppsspp_dir, "cloud": self._cloud_dir}
def _checked_ids(self):
ids = []
for row in range(self.table.rowCount()):
cb = self.table.item(row, 0)
if cb is not None and cb.checkState() == Qt.Checked:
sid = cb.data(Qt.UserRole)
if sid:
ids.append(sid)
return ids
def _on_item_changed(self, item):
if getattr(self, "_populating", False) or item.column() != 0:
return
self._update_delete_enabled()
def _update_delete_enabled(self):
self.delete_btn.setEnabled(bool(self._checked_ids()))
def _on_context_menu(self, pos):
item = self.table.itemAt(pos)
if item is None:
return
row = item.row()
cb = self.table.item(row, 0)
sid = cb.data(Qt.UserRole) if cb is not None else None
if not sid:
id_item = self.table.item(row, 2)
sid = id_item.text() if id_item is not None else None
if not sid:
return
menu = QMenu(self)
menu.addAction("Export…", lambda: self._export_save(sid))
menu.addAction("Delete…", lambda: self._delete_saves([sid]))
menu.exec(self.table.viewport().mapToGlobal(pos))
def _delete_checked(self):
ids = self._checked_ids()
if ids:
self._delete_saves(ids)
def _delete_saves(self, sids):
targets = []
seen = set()
for sid in sids:
for loc in ("psp", "ppsspp", "cloud"):
s = self._data.get(loc, {}).get(sid)
if not s:
continue
p = s.get("path")
if p and p not in seen:
seen.add(p)
targets.append(p)
if not targets:
QMessageBox.information(self, "Delete saves", "No local copies found.")
return
preview = "\n".join(targets[:8])
if len(targets) > 8:
preview += f"\n… (+{len(targets) - 8} more)"
res = QMessageBox.question(
self, "Delete saves",
f"Delete these save folders from disk?\n\n{preview}",
QMessageBox.Yes | QMessageBox.Cancel, QMessageBox.Cancel,
)
if res != QMessageBox.Yes:
return
failed = 0
for p in targets:
if not transfers.remove_path(Path(p)):
failed += 1
if failed:
QMessageBox.warning(self, "Delete incomplete", f"{failed} folder(s) could not be deleted.")
self.refresh()
def _export_save(self, sid):
src = None
for loc in ("psp", "ppsspp", "cloud"):
s = self._data.get(loc, {}).get(sid)
if s and Path(s.get("path", "")).is_dir():
src = s["path"]
break
if not src:
QMessageBox.information(self, "Export save", "No local copy found.")
return
dest = QFileDialog.getExistingDirectory(self, "Export save folder")
if not dest:
return
try:
transfers.copy_tree(Path(src), Path(dest) / sid)
except Exception as exc: # noqa: BLE001
QMessageBox.warning(self, "Export failed", str(exc))
return
QMessageBox.information(self, "Export save", f"Exported to:\n{Path(dest) / sid}")
def refresh(self): def refresh(self):
locs = self._locations() locs = self._locations()
data = {name: {s["id"]: s for s in saves.list_saves(root)} for name, root in locs.items()} data = {name: {s["id"]: s for s in saves.list_saves(root)} for name, root in locs.items()}
self._data = data
all_ids = set() all_ids = set()
for d in data.values(): for d in data.values():
all_ids.update(d) all_ids.update(d)
sort_col = self.table.horizontalHeader().sortIndicatorSection() sort_col = self.table.horizontalHeader().sortIndicatorSection()
sort_order = self.table.horizontalHeader().sortIndicatorOrder() sort_order = self.table.horizontalHeader().sortIndicatorOrder()
self.table.setSortingEnabled(False) self.table.setSortingEnabled(False)
self._populating = True
self.table.setRowCount(0) self.table.setRowCount(0)
for sid in sorted(all_ids): for sid in sorted(all_ids):
row = self.table.rowCount() row = self.table.rowCount()
self.table.insertRow(row) self.table.insertRow(row)
cb = QTableWidgetItem()
cb.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
cb.setCheckState(Qt.Unchecked)
cb.setData(Qt.UserRole, sid)
self.table.setItem(row, 0, cb)
title = sid title = sid
enc = False enc = False
icon = None icon = None
@@ -3436,9 +3546,9 @@ class SavesPage(QWidget):
t.setToolTip(sid) t.setToolTip(sid)
if icon is not None: if icon is not None:
t.setIcon(icon) t.setIcon(icon)
self.table.setItem(row, 0, t) self.table.setItem(row, 1, t)
self.table.setItem(row, 1, QTableWidgetItem(sid)) self.table.setItem(row, 2, QTableWidgetItem(sid))
for col, loc in enumerate(("psp", "ppsspp", "cloud"), start=2): for col, loc in enumerate(("psp", "ppsspp", "cloud"), start=3):
s = data[loc].get(sid) s = data[loc].get(sid)
if s is None: if s is None:
self.table.setItem(row, col, QTableWidgetItem("")) self.table.setItem(row, col, QTableWidgetItem(""))
@@ -3447,9 +3557,11 @@ class SavesPage(QWidget):
item = QTableWidgetItem(stamp) item = QTableWidgetItem(stamp)
item.setToolTip(f"{s['size'] / 1024:.1f} KB") item.setToolTip(f"{s['size'] / 1024:.1f} KB")
self.table.setItem(row, col, item) self.table.setItem(row, col, item)
self._populating = False
self.table.setSortingEnabled(True) self.table.setSortingEnabled(True)
self.table.sortByColumn(sort_col, sort_order) self.table.sortByColumn(sort_col, sort_order)
self.status.setText(f"{len(all_ids)} save(s) across {sum(1 for r in locs.values() if r)} location(s)") self.status.setText(f"{len(all_ids)} save(s) across {sum(1 for r in locs.values() if r)} location(s)")
self._update_delete_enabled()
def sync_now(self, switch_tab=False): def sync_now(self, switch_tab=False):
if self._syncing: if self._syncing: