316 lines
12 KiB
Python
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).")
|