diff --git a/app/gui.py b/app/gui.py index b7037a7..c58667d 100644 --- a/app/gui.py +++ b/app/gui.py @@ -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: