Files
GOGUpdater/src/ui/dialog_game_versions.py
T

316 lines
12 KiB
Python

"""Dialog showing downloaded versions of a game with management actions."""
import shutil
from loguru import logger
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (
QDialog,
QDialogButtonBox,
QGroupBox,
QHBoxLayout,
QLabel,
QMessageBox,
QPushButton,
QTreeWidget,
QTreeWidgetItem,
QVBoxLayout,
QWidget,
)
from src.config import MetadataStore
from src.models import DownloadedInstaller, language_folder_name
COL_FILE = 0
COL_VERSION = 1
COL_LANG = 2
COL_SIZE = 3
COL_DATE = 4
def _fmt_size(size: int) -> str:
for unit in ("B", "KB", "MB", "GB"):
if size < 1024:
return f"{size:.1f} {unit}"
size //= 1024
return f"{size:.1f} GB"
class GameVersionsDialog(QDialog):
"""Shows all downloaded versions/files for a game and lets user manage them."""
def __init__(
self,
game_id: str,
game_title: str,
metadata: MetadataStore,
parent: QWidget | None = None,
) -> None:
super().__init__(parent)
self.game_id = game_id
self.game_title = game_title
self.metadata = metadata
self.base_path = metadata.base_path
self.setWindowTitle(f"Downloaded versions — {game_title}")
self.setMinimumSize(700, 450)
self._setup_ui()
self._populate()
def _setup_ui(self) -> None:
layout = QVBoxLayout(self)
# Header info
self.info_label = QLabel()
layout.addWidget(self.info_label)
# Installers group
inst_group = QGroupBox("Installer files")
inst_layout = QVBoxLayout(inst_group)
self.inst_tree = QTreeWidget()
self.inst_tree.setColumnCount(5)
self.inst_tree.setHeaderLabels(["File", "Version", "Language", "Size", "Downloaded"])
self.inst_tree.header().setStretchLastSection(False)
self.inst_tree.header().resizeSection(COL_FILE, 280)
self.inst_tree.header().resizeSection(COL_VERSION, 100)
self.inst_tree.header().resizeSection(COL_LANG, 100)
self.inst_tree.header().resizeSection(COL_SIZE, 80)
self.inst_tree.header().resizeSection(COL_DATE, 160)
self.inst_tree.setSelectionMode(QTreeWidget.SelectionMode.ExtendedSelection)
self.inst_tree.setSortingEnabled(True)
inst_layout.addWidget(self.inst_tree)
inst_btn_row = QHBoxLayout()
self.delete_selected_btn = QPushButton("Delete selected files")
self.delete_selected_btn.clicked.connect(self._delete_selected_installers)
inst_btn_row.addWidget(self.delete_selected_btn)
self.delete_version_btn = QPushButton("Delete entire version folder…")
self.delete_version_btn.clicked.connect(self._delete_version_folder)
inst_btn_row.addWidget(self.delete_version_btn)
inst_btn_row.addStretch()
inst_layout.addLayout(inst_btn_row)
layout.addWidget(inst_group)
# Bonus group
bonus_group = QGroupBox("Bonus content")
bonus_layout = QVBoxLayout(bonus_group)
self.bonus_tree = QTreeWidget()
self.bonus_tree.setColumnCount(4)
self.bonus_tree.setHeaderLabels(["File", "Name", "Type", "Size"])
self.bonus_tree.header().setStretchLastSection(False)
self.bonus_tree.header().resizeSection(COL_FILE, 280)
self.bonus_tree.header().resizeSection(1, 180)
self.bonus_tree.header().resizeSection(2, 100)
self.bonus_tree.setSelectionMode(QTreeWidget.SelectionMode.ExtendedSelection)
bonus_layout.addWidget(self.bonus_tree)
bonus_btn_row = QHBoxLayout()
self.delete_bonus_btn = QPushButton("Delete selected bonus files")
self.delete_bonus_btn.clicked.connect(self._delete_selected_bonuses)
bonus_btn_row.addWidget(self.delete_bonus_btn)
bonus_btn_row.addStretch()
bonus_layout.addLayout(bonus_btn_row)
layout.addWidget(bonus_group)
# Close button
close_btn = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
close_btn.rejected.connect(self.reject)
layout.addWidget(close_btn)
def _populate(self) -> None:
record = self.metadata.get_game(self.game_id)
if not record:
self.info_label.setText("No downloaded data found for this game.")
return
total_size = sum(i.size for i in record.installers) + sum(b.size for b in record.bonuses)
self.info_label.setText(
f"<b>{self.game_title}</b> — {len(record.installers)} installer file(s), "
f"{len(record.bonuses)} bonus file(s) — total {_fmt_size(total_size)}"
)
# Group installers by version for visual clarity
versions: dict[str, list[DownloadedInstaller]] = {}
for inst in record.installers:
versions.setdefault(inst.version, []).append(inst)
self.inst_tree.clear()
for version in sorted(versions):
ver_node = QTreeWidgetItem()
ver_node.setText(COL_FILE, f"Version {version}")
ver_node.setText(COL_VERSION, version)
bold = QFont()
bold.setBold(True)
ver_node.setFont(COL_FILE, bold)
ver_node.setData(COL_FILE, Qt.ItemDataRole.UserRole, ("version", version))
ver_size = sum(i.size for i in versions[version])
ver_node.setText(COL_SIZE, _fmt_size(ver_size))
for inst in versions[version]:
file_node = QTreeWidgetItem()
file_node.setText(COL_FILE, inst.filename)
file_node.setText(COL_VERSION, inst.version)
file_node.setText(COL_LANG, language_folder_name(inst.language))
file_node.setText(COL_SIZE, _fmt_size(inst.size))
file_node.setText(COL_DATE, inst.downloaded_at[:19].replace("T", " ") if inst.downloaded_at else "")
file_node.setData(COL_FILE, Qt.ItemDataRole.UserRole, ("installer", inst))
# Grey out if file no longer on disk
if not self.metadata.find_installer_file(record, inst):
for col in range(5):
file_node.setForeground(col, self.palette().color(self.foregroundRole()).darker(150))
file_node.setToolTip(COL_FILE, "File not found on disk")
ver_node.addChild(file_node)
ver_node.setExpanded(True)
self.inst_tree.addTopLevelItem(ver_node)
# Bonus files
self.bonus_tree.clear()
for bonus in record.bonuses:
node = QTreeWidgetItem()
node.setText(0, bonus.filename)
node.setText(1, bonus.name)
node.setText(2, bonus.bonus_type)
node.setText(3, _fmt_size(bonus.size))
node.setData(0, Qt.ItemDataRole.UserRole, bonus)
if not self.metadata.find_bonus_file(record, bonus):
for col in range(4):
node.setForeground(col, self.palette().color(self.foregroundRole()).darker(150))
node.setToolTip(0, "File not found on disk")
self.bonus_tree.addTopLevelItem(node)
def _delete_selected_installers(self) -> None:
selected = [
item for item in self.inst_tree.selectedItems()
if item.data(COL_FILE, Qt.ItemDataRole.UserRole) and
item.data(COL_FILE, Qt.ItemDataRole.UserRole)[0] == "installer"
]
if not selected:
QMessageBox.information(self, "Delete", "Select one or more installer files first.")
return
reply = QMessageBox.question(
self,
"Delete files",
f"Delete {len(selected)} selected file(s) from disk?\nMetadata will be updated.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply != QMessageBox.StandardButton.Yes:
return
record = self.metadata.get_game(self.game_id)
if not record:
return
deleted = 0
for item in selected:
inst: DownloadedInstaller = item.data(COL_FILE, Qt.ItemDataRole.UserRole)[1]
path = self.metadata.find_installer_file(record, inst)
if path:
path.unlink()
logger.info(f"Deleted {path}")
deleted += 1
record.installers = [i for i in record.installers if i.filename != inst.filename or i.version != inst.version]
# Update latest_version
if record.installers:
record.latest_version = sorted({i.version for i in record.installers})[-1]
else:
record.latest_version = ""
self.metadata.update_game(record)
self._populate()
QMessageBox.information(self, "Done", f"Deleted {deleted} file(s).")
def _delete_version_folder(self) -> None:
"""Delete an entire version directory after user selects which version."""
record = self.metadata.get_game(self.game_id)
if not record:
return
versions = sorted({i.version for i in record.installers})
if not versions:
QMessageBox.information(self, "Delete", "No versions found in metadata.")
return
# Pick version from selected item if any
selected = self.inst_tree.currentItem()
preselected_version = ""
if selected:
data = selected.data(COL_FILE, Qt.ItemDataRole.UserRole)
if data and data[0] == "version":
preselected_version = data[1]
elif data and data[0] == "installer":
preselected_version = data[1].version
version = preselected_version or versions[-1]
version_dirs = [d / version for d in self.metadata.candidate_dirs(record) if (d / version).exists()]
reply = QMessageBox.question(
self,
"Delete version folder",
f"Delete entire version folder '{version}' for '{self.game_title}'?\n\n"
f"Path: {version_dirs[0] if version_dirs else '(not found on disk)'}\n\n"
"This will remove all files in that folder from disk.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply != QMessageBox.StandardButton.Yes:
return
for version_dir in version_dirs:
shutil.rmtree(version_dir)
logger.info(f"Deleted version folder {version_dir}")
record.installers = [i for i in record.installers if i.version != version]
if record.installers:
record.latest_version = sorted({i.version for i in record.installers})[-1]
else:
record.latest_version = ""
self.metadata.update_game(record)
self._populate()
def _delete_selected_bonuses(self) -> None:
selected = self.bonus_tree.selectedItems()
if not selected:
QMessageBox.information(self, "Delete", "Select one or more bonus files first.")
return
reply = QMessageBox.question(
self,
"Delete bonus files",
f"Delete {len(selected)} bonus file(s) from disk?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply != QMessageBox.StandardButton.Yes:
return
record = self.metadata.get_game(self.game_id)
if not record:
return
deleted = 0
for item in selected:
bonus = item.data(0, Qt.ItemDataRole.UserRole)
path = self.metadata.find_bonus_file(record, bonus)
if path:
path.unlink()
logger.info(f"Deleted bonus {path}")
deleted += 1
record.bonuses = [b for b in record.bonuses if b.filename != bonus.filename]
self.metadata.update_game(record)
self._populate()
QMessageBox.information(self, "Done", f"Deleted {deleted} bonus file(s).")