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:
+121
-9
@@ -3366,22 +3366,32 @@ class SavesPage(QWidget):
|
||||
self.sync_btn = QPushButton("Sync now")
|
||||
self.sync_btn.clicked.connect(lambda: self.sync_now(True))
|
||||
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)
|
||||
layout.addLayout(bar)
|
||||
|
||||
self.table = QTableWidget(0, 5)
|
||||
self.table.setHorizontalHeaderLabels(["Save", "ID", "PSP", "PPSSPP", "Cloud"])
|
||||
self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
|
||||
self.table = QTableWidget(0, 6)
|
||||
self.table.setHorizontalHeaderLabels(["", "Save", "ID", "PSP", "PPSSPP", "Cloud"])
|
||||
self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
|
||||
self.table.verticalHeader().setVisible(False)
|
||||
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.table.setSortingEnabled(True)
|
||||
self.table.setIconSize(QSize(40, 32))
|
||||
self.table.horizontalHeader().setSortIndicator(0, Qt.AscendingOrder)
|
||||
self.table.setIconSize(QSize(56, 40))
|
||||
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)
|
||||
|
||||
self.status = QLabel("")
|
||||
layout.addWidget(self.status)
|
||||
self._data = {}
|
||||
self._populating = False
|
||||
|
||||
def _save_icon(self, folder):
|
||||
"""Return a small QIcon from a save folder's ICON0.PNG, or None."""
|
||||
@@ -3393,7 +3403,7 @@ class SavesPage(QWidget):
|
||||
pm = QPixmap(str(p))
|
||||
if pm.isNull():
|
||||
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):
|
||||
self._psp_dir = psp_dir or ""
|
||||
@@ -3404,19 +3414,119 @@ class SavesPage(QWidget):
|
||||
def _locations(self):
|
||||
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):
|
||||
locs = self._locations()
|
||||
data = {name: {s["id"]: s for s in saves.list_saves(root)} for name, root in locs.items()}
|
||||
self._data = data
|
||||
all_ids = set()
|
||||
for d in data.values():
|
||||
all_ids.update(d)
|
||||
sort_col = self.table.horizontalHeader().sortIndicatorSection()
|
||||
sort_order = self.table.horizontalHeader().sortIndicatorOrder()
|
||||
self.table.setSortingEnabled(False)
|
||||
self._populating = True
|
||||
self.table.setRowCount(0)
|
||||
for sid in sorted(all_ids):
|
||||
row = self.table.rowCount()
|
||||
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
|
||||
enc = False
|
||||
icon = None
|
||||
@@ -3436,9 +3546,9 @@ class SavesPage(QWidget):
|
||||
t.setToolTip(sid)
|
||||
if icon is not None:
|
||||
t.setIcon(icon)
|
||||
self.table.setItem(row, 0, t)
|
||||
self.table.setItem(row, 1, QTableWidgetItem(sid))
|
||||
for col, loc in enumerate(("psp", "ppsspp", "cloud"), start=2):
|
||||
self.table.setItem(row, 1, t)
|
||||
self.table.setItem(row, 2, QTableWidgetItem(sid))
|
||||
for col, loc in enumerate(("psp", "ppsspp", "cloud"), start=3):
|
||||
s = data[loc].get(sid)
|
||||
if s is None:
|
||||
self.table.setItem(row, col, QTableWidgetItem(""))
|
||||
@@ -3447,9 +3557,11 @@ class SavesPage(QWidget):
|
||||
item = QTableWidgetItem(stamp)
|
||||
item.setToolTip(f"{s['size'] / 1024:.1f} KB")
|
||||
self.table.setItem(row, col, item)
|
||||
self._populating = False
|
||||
self.table.setSortingEnabled(True)
|
||||
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._update_delete_enabled()
|
||||
|
||||
def sync_now(self, switch_tab=False):
|
||||
if self._syncing:
|
||||
|
||||
Reference in New Issue
Block a user