Add MTGA Assistant: native Linux deck and game tracker

PySide6 app that follows MTG Arena's Player.log under Steam/Proton and shows
my remaining library with draw odds, my zones, and everything revealed about
the opponent (board, graveyard, exile, known hand/library, seen this match).

- Auto-detects the Steam library, Proton prefix log and MTGA install
- Card names/costs/types from MTGA's local SQLite card database
- Tracks card identity across zone changes, shuffles and Bo3 games
- Scryfall card images with a lazy, size-capped LRU disk cache
- Live log following with restart detection, plus --replay mode
- Flags log gaps where MTGA omits oversized game-state messages

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aeth3randClaude Opus 5.5 committed 2026-10-16 09:56:12 -04:00
1 parent 734336168c
commit 7dc2ce0da8
21 files changed
+2736

No files matched your search

+80
View File
@@ -0,0 +1,80 @@
"""Large image of the hovered card, with a text fallback while it downloads."""
from __future__ import annotations
from PySide6.QtCore import QSize, Qt
from PySide6.QtGui import QPixmap, QResizeEvent
from PySide6.QtWidgets import QLabel, QSizePolicy, QVBoxLayout, QWidget
from ..carddb import CardDB
from ..images import ImageCache
class CardPreview(QWidget):
def __init__(self, cards: CardDB, images: ImageCache, parent=None):
super().__init__(parent)
self.cards = cards
self.images = images
self.grp_id: int | None = None
self._pixmap: QPixmap | None = None
self.image = QLabel()
self.image.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.image.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
self.image.setMinimumSize(200, 280)
self.caption = QLabel()
self.caption.setWordWrap(True)
self.caption.setAlignment(Qt.AlignmentFlag.AlignCenter)
lay = QVBoxLayout(self)
lay.setContentsMargins(4, 4, 4, 4)
lay.addWidget(self.image, 1)
lay.addWidget(self.caption)
images.image_ready.connect(self._on_image_ready)
self._show_placeholder("Hover a card to preview it")
def show_card(self, grp_id: int) -> None:
if grp_id == self.grp_id:
return
self.grp_id = grp_id
info = self.cards.get(grp_id)
details = [info.type_line]
if info.power or info.toughness:
details.append(f"{info.power}/{info.toughness}")
if info.set_code:
details.append(f"{info.set_code} #{info.collector_number}")
self.caption.setText(f"<b>{info.name}</b><br>{' · '.join(d for d in details if d)}")
pm = self.images.pixmap(grp_id)
if pm is not None:
self._pixmap = pm
self._rescale()
elif self.images.is_failed(grp_id):
self._show_placeholder("No image available")
else:
self._show_placeholder("Loading image…")
def _show_placeholder(self, text: str) -> None:
self._pixmap = None
self.image.setPixmap(QPixmap())
self.image.setText(text)
def _on_image_ready(self, grp_id: int) -> None:
if grp_id == self.grp_id:
self.grp_id = None
self.show_card(grp_id)
def _rescale(self) -> None:
if self._pixmap is None:
return
self.image.setPixmap(
self._pixmap.scaled(self.image.size(), Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation)
)
def resizeEvent(self, event: QResizeEvent) -> None:
super().resizeEvent(event)
self._rescale()
def sizeHint(self) -> QSize:
return QSize(320, 600)