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 = 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:
|
||||||
|
|||||||
Reference in New Issue
Block a user