Add dedicated Pruning tab with strategies and per-game overrides

This commit is contained in:
2026-05-13 16:58:44 +02:00
parent 04662b96ec
commit 16d5c2857d
9 changed files with 447 additions and 48 deletions
+13
View File
@@ -1,5 +1,18 @@
# Changelog # Changelog
## [1.1.0] — 2026-05-13
### Features
- Dedicated Pruning tab — global keep-count setting with four strategies: latest N only, latest N + oldest, latest N + one per year, latest N + one per year + oldest
- Per-game pruning overrides — exclude individual games from global pruning or set a custom keep count, configurable via the existing game settings dialog
- Prune confirmation dialog shows a per-platform preview (Windows and Linux pruned independently — no cross-platform version comparison)
### Changes
- Pruning controls removed from Status tab and moved to the new Pruning tab
- Fixed bug in `prune_old_versions()`: installer list was reassigned inside the loop and `latest_version` was not updated after pruning
## [1.0.1] — 2026-05-12 ## [1.0.1] — 2026-05-12
### Improvements ### Improvements
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "gogupdater" name = "gogupdater"
version = "1.0.1" version = "1.1.0"
description = "" description = ""
authors = [ authors = [
{name = "Jan Doubravský",email = "jan.doubravsky@gmail.com"} {name = "Jan Doubravský",email = "jan.doubravsky@gmail.com"}
+1 -1
View File
@@ -1,2 +1,2 @@
"""Auto-generated — do not edit manually.""" """Auto-generated — do not edit manually."""
__version__ = "1.0.0" __version__ = "1.1.0"
+64 -9
View File
@@ -5,7 +5,7 @@ from pathlib import Path
from loguru import logger from loguru import logger
from src.models import DownloadedInstaller, GameRecord, GameSettings, InstallerType, LANGUAGE_NAMES, language_folder_name from src.models import DownloadedInstaller, GameRecord, GameSettings, InstallerType, LANGUAGE_NAMES, PruneStrategy, language_folder_name
DEFAULT_CONFIG_DIR = Path.home() / ".config" / "gogupdater" DEFAULT_CONFIG_DIR = Path.home() / ".config" / "gogupdater"
METADATA_FILENAME = "gogupdater.json" METADATA_FILENAME = "gogupdater.json"
@@ -25,6 +25,8 @@ class AppConfig:
self.include_bonus: bool = False self.include_bonus: bool = False
self.game_settings: dict[str, GameSettings] = {} self.game_settings: dict[str, GameSettings] = {}
self.version_decisions: dict[str, bool] = {} self.version_decisions: dict[str, bool] = {}
self.prune_keep_count: int = 1
self.prune_strategy: PruneStrategy = PruneStrategy.LATEST_N
self._load() self._load()
def _load(self) -> None: def _load(self) -> None:
@@ -43,6 +45,11 @@ class AppConfig:
for gid, gs in data.get("game_settings", {}).items() for gid, gs in data.get("game_settings", {}).items()
} }
self.version_decisions = data.get("version_decisions", {}) self.version_decisions = data.get("version_decisions", {})
self.prune_keep_count = int(data.get("prune_keep_count", 1))
try:
self.prune_strategy = PruneStrategy(data.get("prune_strategy", PruneStrategy.LATEST_N.value))
except ValueError:
self.prune_strategy = PruneStrategy.LATEST_N
except (json.JSONDecodeError, OSError): except (json.JSONDecodeError, OSError):
logger.warning("Failed to read config.json, using defaults") logger.warning("Failed to read config.json, using defaults")
@@ -61,6 +68,8 @@ class AppConfig:
if not gs.is_default() if not gs.is_default()
}, },
"version_decisions": self.version_decisions, "version_decisions": self.version_decisions,
"prune_keep_count": self.prune_keep_count,
"prune_strategy": self.prune_strategy.value,
} }
self.config_file.write_text(json.dumps(data, indent=2), encoding="utf-8") self.config_file.write_text(json.dumps(data, indent=2), encoding="utf-8")
@@ -149,33 +158,79 @@ class MetadataStore:
return [] return []
return list({i.version for i in record.installers}) return list({i.version for i in record.installers})
def prune_old_versions(self, game_id: str, keep_latest: int = 1) -> list[Path]: def prune_old_versions(
"""Remove old version directories, keeping the N most recent. Returns deleted paths.""" self,
game_id: str,
keep_latest: int = 1,
strategy: PruneStrategy = PruneStrategy.LATEST_N,
) -> list[Path]:
"""Remove old version directories according to strategy. Returns deleted paths."""
record = self.games.get(game_id) record = self.games.get(game_id)
if not record: if not record:
return [] return []
versions = sorted({i.version for i in record.installers}) all_versions = sorted({i.version for i in record.installers})
if len(versions) <= keep_latest: versions_to_keep = self._select_versions_to_keep(record, all_versions, keep_latest, strategy)
versions_to_remove = [v for v in all_versions if v not in versions_to_keep]
if not versions_to_remove:
return [] return []
versions_to_remove = versions[:-keep_latest] import shutil
deleted: list[Path] = []
deleted: list[Path] = []
for version in versions_to_remove: for version in versions_to_remove:
version_dir = self.base_path / record.name / version version_dir = self.base_path / record.name / version
if version_dir.exists(): if version_dir.exists():
import shutil
shutil.rmtree(version_dir) shutil.rmtree(version_dir)
deleted.append(version_dir) deleted.append(version_dir)
logger.info(f"Pruned {version_dir}") logger.info(f"Pruned {version_dir}")
record.installers = [i for i in record.installers if i.version not in versions_to_remove] record.installers = [i for i in record.installers if i.version not in versions_to_remove]
remaining = sorted({i.version for i in record.installers})
record.latest_version = remaining[-1] if remaining else ""
self.save() self.save()
return deleted return deleted
@staticmethod
def _select_versions_to_keep(
record: "GameRecord",
all_versions: list[str],
keep_latest: int,
strategy: PruneStrategy,
) -> set[str]:
if len(all_versions) <= keep_latest:
return set(all_versions)
keep: set[str] = set(all_versions[-keep_latest:])
if strategy in (PruneStrategy.LATEST_N_OLDEST, PruneStrategy.LATEST_N_YEARLY_OLDEST):
keep.add(all_versions[0])
if strategy in (PruneStrategy.YEARLY, PruneStrategy.LATEST_N_YEARLY_OLDEST):
keep.update(MetadataStore._yearly_representatives(record, all_versions))
return keep
@staticmethod
def _yearly_representatives(record: "GameRecord", all_versions: list[str]) -> set[str]:
"""Return one version per calendar year (the latest in that year by sorted order)."""
version_year: dict[str, int] = {}
for inst in record.installers:
if inst.downloaded_at and inst.version not in version_year:
try:
version_year[inst.version] = int(inst.downloaded_at[:4])
except (ValueError, IndexError):
pass
year_rep: dict[int, str] = {}
for version in all_versions:
year = version_year.get(version)
if year is not None:
if year not in year_rep or all_versions.index(version) > all_versions.index(year_rep[year]):
year_rep[year] = version
return set(year_rep.values())
def verify_metadata(self) -> int: def verify_metadata(self) -> int:
"""Verify that recorded files still exist on disk. """Verify that recorded files still exist on disk.
+22 -1
View File
@@ -87,6 +87,13 @@ class GameStatus(Enum):
UNVERSIONED = "unversioned" UNVERSIONED = "unversioned"
class PruneStrategy(Enum):
LATEST_N = "latest_n" # keep N most recent versions only
LATEST_N_OLDEST = "latest_n_oldest" # keep N most recent + the oldest
YEARLY = "yearly" # keep N most recent + one per calendar year
LATEST_N_YEARLY_OLDEST = "latest_n_yearly_oldest" # keep N most recent + one per year + oldest
@dataclass @dataclass
class InstallerInfo: class InstallerInfo:
"""Installer metadata from GOG API.""" """Installer metadata from GOG API."""
@@ -276,6 +283,8 @@ class GameSettings:
languages: list[str] | None = None languages: list[str] | None = None
english_only: bool | None = None english_only: bool | None = None
include_bonus: bool | None = None include_bonus: bool | None = None
prune_exclude: bool | None = None # True = skip in global prune
prune_keep_count: int | None = None # override global keep count
def to_dict(self) -> dict: def to_dict(self) -> dict:
data: dict = {} data: dict = {}
@@ -285,6 +294,10 @@ class GameSettings:
data["english_only"] = self.english_only data["english_only"] = self.english_only
if self.include_bonus is not None: if self.include_bonus is not None:
data["include_bonus"] = self.include_bonus data["include_bonus"] = self.include_bonus
if self.prune_exclude is not None:
data["prune_exclude"] = self.prune_exclude
if self.prune_keep_count is not None:
data["prune_keep_count"] = self.prune_keep_count
return data return data
@classmethod @classmethod
@@ -293,11 +306,19 @@ class GameSettings:
languages=data.get("languages"), languages=data.get("languages"),
english_only=data.get("english_only"), english_only=data.get("english_only"),
include_bonus=data.get("include_bonus"), include_bonus=data.get("include_bonus"),
prune_exclude=data.get("prune_exclude"),
prune_keep_count=data.get("prune_keep_count"),
) )
def is_default(self) -> bool: def is_default(self) -> bool:
"""True if all values are None (no overrides).""" """True if all values are None (no overrides)."""
return self.languages is None and self.english_only is None and self.include_bonus is None return (
self.languages is None
and self.english_only is None
and self.include_bonus is None
and self.prune_exclude is None
and self.prune_keep_count is None
)
@dataclass @dataclass
+49
View File
@@ -6,9 +6,11 @@ from PySide6.QtWidgets import (
QDialog, QDialog,
QDialogButtonBox, QDialogButtonBox,
QGroupBox, QGroupBox,
QHBoxLayout,
QLabel, QLabel,
QListWidget, QListWidget,
QListWidgetItem, QListWidgetItem,
QSpinBox,
QVBoxLayout, QVBoxLayout,
QWidget, QWidget,
) )
@@ -89,12 +91,48 @@ class GameSettingsDialog(QDialog):
bonus_layout.addWidget(self.bonus_cb) bonus_layout.addWidget(self.bonus_cb)
layout.addWidget(self.bonus_group) layout.addWidget(self.bonus_group)
# Pruning override
self.prune_group = QGroupBox("Override pruning")
self.prune_group.setCheckable(True)
gs_prune_active = self.settings.prune_exclude is not None or self.settings.prune_keep_count is not None
self.prune_group.setChecked(gs_prune_active)
prune_layout = QVBoxLayout(self.prune_group)
self.prune_exclude_cb = QCheckBox("Exclude from global pruning")
self.prune_exclude_cb.setChecked(bool(self.settings.prune_exclude))
prune_layout.addWidget(self.prune_exclude_cb)
keep_row = QHBoxLayout()
self.prune_keep_cb = QCheckBox("Override keep count:")
self.prune_keep_cb.setChecked(self.settings.prune_keep_count is not None)
keep_row.addWidget(self.prune_keep_cb)
self.prune_keep_spin = QSpinBox()
self.prune_keep_spin.setRange(1, 20)
self.prune_keep_spin.setValue(
self.settings.prune_keep_count if self.settings.prune_keep_count is not None
else self.config.prune_keep_count
)
self.prune_keep_spin.setFixedWidth(60)
self.prune_keep_spin.setEnabled(self.prune_keep_cb.isChecked())
keep_row.addWidget(self.prune_keep_spin)
keep_row.addStretch()
prune_layout.addLayout(keep_row)
self.prune_exclude_cb.toggled.connect(self._on_prune_exclude_toggled)
self.prune_keep_cb.toggled.connect(self.prune_keep_spin.setEnabled)
layout.addWidget(self.prune_group)
# Buttons # Buttons
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(self._accept) buttons.accepted.connect(self._accept)
buttons.rejected.connect(self.reject) buttons.rejected.connect(self.reject)
layout.addWidget(buttons) layout.addWidget(buttons)
def _on_prune_exclude_toggled(self, checked: bool) -> None:
if checked:
self.prune_keep_cb.setChecked(False)
self.prune_keep_spin.setEnabled(False)
def _accept(self) -> None: def _accept(self) -> None:
# English only # English only
english_only: bool | None = None english_only: bool | None = None
@@ -118,10 +156,21 @@ class GameSettingsDialog(QDialog):
if self.bonus_group.isChecked(): if self.bonus_group.isChecked():
include_bonus = self.bonus_cb.isChecked() include_bonus = self.bonus_cb.isChecked()
# Pruning
prune_exclude: bool | None = None
prune_keep_count: int | None = None
if self.prune_group.isChecked():
if self.prune_exclude_cb.isChecked():
prune_exclude = True
if self.prune_keep_cb.isChecked() and not self.prune_exclude_cb.isChecked():
prune_keep_count = self.prune_keep_spin.value()
new_settings = GameSettings( new_settings = GameSettings(
languages=languages, languages=languages,
english_only=english_only, english_only=english_only,
include_bonus=include_bonus, include_bonus=include_bonus,
prune_exclude=prune_exclude,
prune_keep_count=prune_keep_count,
) )
self.config.set_game_settings(self.game_id, new_settings) self.config.set_game_settings(self.game_id, new_settings)
logger.info(f"Per-game settings saved for {self.game_id}: {new_settings}") logger.info(f"Per-game settings saved for {self.game_id}: {new_settings}")
+3
View File
@@ -10,6 +10,7 @@ from src.constants import APP_TITLE
from src.ui.tab_auth import AuthTab from src.ui.tab_auth import AuthTab
from src.ui.tab_languages import LanguagesTab from src.ui.tab_languages import LanguagesTab
from src.ui.tab_library import LibraryTab from src.ui.tab_library import LibraryTab
from src.ui.tab_prune import PruneTab
from src.ui.tab_settings import SettingsTab from src.ui.tab_settings import SettingsTab
from src.ui.tab_status import StatusTab from src.ui.tab_status import StatusTab
@@ -38,12 +39,14 @@ class MainWindow(QMainWindow):
self.languages_tab = LanguagesTab(self.config) self.languages_tab = LanguagesTab(self.config)
self.settings_tab = SettingsTab(self.api, self.config) self.settings_tab = SettingsTab(self.api, self.config)
self.status_tab = StatusTab(self.api, self.config) self.status_tab = StatusTab(self.api, self.config)
self.prune_tab = PruneTab(self.config)
self.tabs.addTab(self.auth_tab, "Login") self.tabs.addTab(self.auth_tab, "Login")
self.tabs.addTab(self.library_tab, "Library") self.tabs.addTab(self.library_tab, "Library")
self.tabs.addTab(self.languages_tab, "Languages") self.tabs.addTab(self.languages_tab, "Languages")
self.tabs.addTab(self.settings_tab, "Settings") self.tabs.addTab(self.settings_tab, "Settings")
self.tabs.addTab(self.status_tab, "Status") self.tabs.addTab(self.status_tab, "Status")
self.tabs.addTab(self.prune_tab, "Pruning")
self._update_tab_states() self._update_tab_states()
+293
View File
@@ -0,0 +1,293 @@
"""Pruning tab — manage how many old installer versions to keep."""
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QButtonGroup,
QGroupBox,
QHBoxLayout,
QHeaderView,
QLabel,
QMessageBox,
QPushButton,
QRadioButton,
QSpinBox,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
QWidget,
)
from loguru import logger
from src.config import AppConfig, MetadataStore
from src.models import PruneStrategy
COL_GAME = 0
COL_WIN = 1
COL_LIN = 2
COL_OVERRIDE = 3
COL_BTN = 4
_STRATEGY_LABELS = {
PruneStrategy.LATEST_N: "Latest N only",
PruneStrategy.LATEST_N_OLDEST: "Latest N + oldest",
PruneStrategy.YEARLY: "Latest N + one per year",
PruneStrategy.LATEST_N_YEARLY_OLDEST: "Latest N + one per year + oldest",
}
class PruneTab(QWidget):
"""Tab for configuring and running installer version pruning."""
def __init__(self, config: AppConfig, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.config = config
self._setup_ui()
def _setup_ui(self) -> None:
layout = QVBoxLayout(self)
# --- Global settings ---
settings_group = QGroupBox("Global Pruning Settings")
sg_layout = QVBoxLayout(settings_group)
keep_row = QHBoxLayout()
keep_row.addWidget(QLabel("Keep last"))
self.keep_spin = QSpinBox()
self.keep_spin.setRange(1, 20)
self.keep_spin.setValue(self.config.prune_keep_count)
self.keep_spin.setFixedWidth(60)
keep_row.addWidget(self.keep_spin)
keep_row.addWidget(QLabel("version(s)"))
keep_row.addStretch()
sg_layout.addLayout(keep_row)
strategy_row = QHBoxLayout()
strategy_row.addWidget(QLabel("Strategy:"))
self._strategy_group = QButtonGroup(self)
for strategy, label in _STRATEGY_LABELS.items():
rb = QRadioButton(label)
rb.setProperty("strategy", strategy)
rb.setChecked(strategy == self.config.prune_strategy)
self._strategy_group.addButton(rb)
strategy_row.addWidget(rb)
strategy_row.addStretch()
sg_layout.addLayout(strategy_row)
layout.addWidget(settings_group)
# --- Action buttons ---
btn_row = QHBoxLayout()
refresh_btn = QPushButton("Refresh")
refresh_btn.setFixedWidth(100)
refresh_btn.clicked.connect(self.refresh)
btn_row.addWidget(refresh_btn)
btn_row.addStretch()
prune_all_btn = QPushButton("Prune All")
prune_all_btn.setFixedWidth(120)
prune_all_btn.clicked.connect(self._prune_all)
btn_row.addWidget(prune_all_btn)
layout.addLayout(btn_row)
# --- Game list ---
self.tree = QTreeWidget()
self.tree.setColumnCount(5)
self.tree.setHeaderLabels(["Game", "Windows", "Linux", "Override", ""])
self.tree.header().setSectionResizeMode(COL_GAME, QHeaderView.ResizeMode.Stretch)
self.tree.header().setSectionResizeMode(COL_WIN, QHeaderView.ResizeMode.ResizeToContents)
self.tree.header().setSectionResizeMode(COL_LIN, QHeaderView.ResizeMode.ResizeToContents)
self.tree.header().setSectionResizeMode(COL_OVERRIDE, QHeaderView.ResizeMode.ResizeToContents)
self.tree.header().setSectionResizeMode(COL_BTN, QHeaderView.ResizeMode.Fixed)
self.tree.header().resizeSection(COL_BTN, 80)
self.tree.setSortingEnabled(True)
layout.addWidget(self.tree)
self.status_label = QLabel("")
layout.addWidget(self.status_label)
# Connect settings signals after UI is built
self.keep_spin.valueChanged.connect(self._on_keep_count_changed)
self._strategy_group.buttonToggled.connect(self._on_strategy_changed)
# --- Settings persistence ---
def _on_keep_count_changed(self, value: int) -> None:
self.config.prune_keep_count = value
self.config.save()
def _on_strategy_changed(self) -> None:
for btn in self._strategy_group.buttons():
if btn.isChecked():
self.config.prune_strategy = btn.property("strategy")
self.config.save()
return
# --- Game list ---
def refresh(self) -> None:
"""Reload local metadata and repopulate the game list."""
self.tree.setSortingEnabled(False)
self.tree.clear()
win_store = MetadataStore(self.config.windows_path) if self.config.windows_path else None
lin_store = MetadataStore(self.config.linux_path) if self.config.linux_path else None
all_games: dict[str, str] = {}
if win_store:
for gid, rec in win_store.games.items():
all_games[gid] = rec.name
if lin_store:
for gid, rec in lin_store.games.items():
all_games.setdefault(gid, rec.name)
for game_id, name in sorted(all_games.items(), key=lambda x: x[1].lower()):
win_rec = win_store.games.get(game_id) if win_store else None
lin_rec = lin_store.games.get(game_id) if lin_store else None
win_versions = sorted({i.version for i in win_rec.installers}) if win_rec else []
lin_versions = sorted({i.version for i in lin_rec.installers}) if lin_rec else []
gs = self.config.get_game_settings(game_id)
item = QTreeWidgetItem()
item.setText(COL_GAME, name)
item.setData(COL_GAME, Qt.ItemDataRole.UserRole, game_id)
item.setText(COL_WIN, self._format_versions(win_versions))
item.setText(COL_LIN, self._format_versions(lin_versions))
if gs.prune_exclude:
item.setText(COL_OVERRIDE, "Excluded")
elif gs.prune_keep_count is not None:
item.setText(COL_OVERRIDE, f"Keep {gs.prune_keep_count}")
else:
item.setText(COL_OVERRIDE, "")
self.tree.addTopLevelItem(item)
effective_keep = gs.prune_keep_count if gs.prune_keep_count is not None else self.config.prune_keep_count
can_prune = (len(win_versions) > effective_keep or len(lin_versions) > effective_keep) and not gs.prune_exclude
btn = QPushButton("Excluded" if gs.prune_exclude else "Prune")
btn.setEnabled(can_prune)
btn.clicked.connect(lambda _checked=False, gid=game_id: self._prune_game(gid))
self.tree.setItemWidget(item, COL_BTN, btn)
self.tree.setSortingEnabled(True)
self.status_label.setText(f"Showing {self.tree.topLevelItemCount()} game(s) with local installers.")
@staticmethod
def _format_versions(versions: list[str]) -> str:
if not versions:
return ""
if len(versions) <= 4:
return ", ".join(versions)
return f"{', '.join(versions[:3])}, … ({len(versions)} total)"
# --- Prune actions ---
def _effective_keep(self, game_id: str) -> int:
gs = self.config.get_game_settings(game_id)
return gs.prune_keep_count if gs.prune_keep_count is not None else self.config.prune_keep_count
def _prune_preview(
self, game_id: str, keep: int, strategy: PruneStrategy
) -> list[tuple[str, list[str], list[str]]]:
"""Return [(platform_label, to_keep, to_delete)] — each platform evaluated independently."""
result = []
for label, path_str in [("Windows", self.config.windows_path), ("Linux", self.config.linux_path)]:
if not path_str:
continue
store = MetadataStore(path_str)
record = store.games.get(game_id)
if not record:
continue
all_versions = sorted({i.version for i in record.installers})
if not all_versions:
continue
keep_set = MetadataStore._select_versions_to_keep(record, all_versions, keep, strategy)
result.append((
label,
[v for v in all_versions if v in keep_set],
[v for v in all_versions if v not in keep_set],
))
return result
def _prune_game(self, game_id: str) -> None:
gs = self.config.get_game_settings(game_id)
if gs.prune_exclude:
return
keep = self._effective_keep(game_id)
strategy = self.config.prune_strategy
name = self._game_name(game_id)
preview = self._prune_preview(game_id, keep, strategy)
if not any(to_delete for _, _, to_delete in preview):
self.status_label.setText(f"'{name}': nothing to prune.")
return
lines = [f"Prune '{name}'?", f"Strategy: {_STRATEGY_LABELS[strategy]}", ""]
for platform_label, to_keep, to_delete in preview:
lines.append(f"{platform_label}:")
lines.append(f" Keep: {', '.join(to_keep) or ''}")
if to_delete:
lines.append(f" Delete: {', '.join(to_delete)}")
lines.append("")
reply = QMessageBox.question(
self,
"Prune",
"\n".join(lines).rstrip(),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply != QMessageBox.StandardButton.Yes:
return
total = 0
for path_str in [self.config.windows_path, self.config.linux_path]:
if path_str:
store = MetadataStore(path_str)
deleted = store.prune_old_versions(game_id, keep, strategy)
total += len(deleted)
if deleted:
logger.info(f"Pruned {len(deleted)} folder(s) for '{name}' in {path_str}")
self.status_label.setText(f"Pruned {total} version folder(s) for '{name}'.")
self.refresh()
def _prune_all(self) -> None:
keep_count = self.config.prune_keep_count
strategy = self.config.prune_strategy
reply = QMessageBox.question(
self,
"Prune All",
f"Run global pruning for all non-excluded games?\n\nKeep: {keep_count} most recent ({_STRATEGY_LABELS[strategy]})",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply != QMessageBox.StandardButton.Yes:
return
total_folders = 0
total_games = 0
for path_str in [self.config.windows_path, self.config.linux_path]:
if not path_str:
continue
store = MetadataStore(path_str)
for game_id in list(store.games.keys()):
gs = self.config.get_game_settings(game_id)
if gs.prune_exclude:
continue
effective_keep = gs.prune_keep_count if gs.prune_keep_count is not None else keep_count
deleted = store.prune_old_versions(game_id, effective_keep, strategy)
if deleted:
total_folders += len(deleted)
total_games += 1
self.status_label.setText(f"Pruned {total_folders} folder(s) from {total_games} game(s).")
self.refresh()
def _game_name(self, game_id: str) -> str:
for i in range(self.tree.topLevelItemCount()):
item = self.tree.topLevelItem(i)
if item and item.data(COL_GAME, Qt.ItemDataRole.UserRole) == game_id:
return item.text(COL_GAME)
return game_id
-35
View File
@@ -6,7 +6,6 @@ from PySide6.QtCore import Qt
from PySide6.QtGui import QBrush, QColor from PySide6.QtGui import QBrush, QColor
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QCheckBox, QCheckBox,
QComboBox,
QHBoxLayout, QHBoxLayout,
QHeaderView, QHeaderView,
QLabel, QLabel,
@@ -90,15 +89,6 @@ class StatusTab(QWidget):
self.download_btn.setEnabled(False) self.download_btn.setEnabled(False)
controls.addWidget(self.download_btn) controls.addWidget(self.download_btn)
self.prune_btn = QPushButton("Prune Old Versions")
self.prune_btn.clicked.connect(self.prune_versions)
controls.addWidget(self.prune_btn)
self.prune_keep_combo = QComboBox()
self.prune_keep_combo.addItems(["Keep 1", "Keep 2", "Keep 3"])
controls.addWidget(self.prune_keep_combo)
self.show_unknown_cb = QCheckBox("Show unversioned/unknown") self.show_unknown_cb = QCheckBox("Show unversioned/unknown")
self.show_unknown_cb.setChecked(False) self.show_unknown_cb.setChecked(False)
self.show_unknown_cb.toggled.connect(self._repopulate_current) self.show_unknown_cb.toggled.connect(self._repopulate_current)
@@ -584,28 +574,3 @@ class StatusTab(QWidget):
dialog = GameVersionsDialog(game_id, game_name, metadata, self) dialog = GameVersionsDialog(game_id, game_name, metadata, self)
dialog.exec() dialog.exec()
def prune_versions(self) -> None:
"""Remove old installer versions for the currently visible platform."""
platform = self._current_platform()
base_path = self._base_path_for(platform)
if not base_path:
self.status_label.setText("No path configured for this platform.")
return
keep = self.prune_keep_combo.currentIndex() + 1
reply = QMessageBox.question(
self,
"Prune Old Versions",
f"This will delete all but the {keep} most recent version(s) of each managed game "
f"in the {platform.value.capitalize()} path.\n\nContinue?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply != QMessageBox.StandardButton.Yes:
return
metadata = MetadataStore(base_path)
total_deleted = sum(
len(metadata.prune_old_versions(game_id, keep_latest=keep))
for game_id in self.config.managed_game_ids
)
self.status_label.setText(f"Pruned {total_deleted} old version(s).")