Make the app cross-platform and add a Windows build
- paths: per-platform detection behind an injectable Env. Windows finds Player.log in the user profile and the install via Steam (registry), the standalone installer, Epic, or the uninstall registry. App dirs come from platformdirs (unchanged XDG locations on Linux). - logtail: open Player.log only while reading, so Windows can still rotate it when MTGA restarts. - carddb: on Windows read a private copy of the card database so MTGA's updater can replace the original. - App icon (SVG source + generated ICO/PNG), Windows taskbar identity, and a Windows-safe timer glyph. - PyInstaller spec, launcher and PowerShell build script producing a one-folder windowed build and zip. - Tests for path detection, the log follower and the card-DB copy; README covers Windows usage, building and per-OS locations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
171fc64ca9
commit
e82f8d1858
19 files changed
+685
-54
No files matched your search
@@ -0,0 +1,13 @@
|
||||
# Build the Windows app folder and a zip of it: dist\MTGA Assistant\ and dist\MTGA-Assistant-windows.zip
|
||||
# Requires uv (https://docs.astral.sh/uv/). Run from anywhere:
|
||||
# powershell -ExecutionPolicy Bypass -File packaging\build_windows.ps1
|
||||
$ErrorActionPreference = "Stop"
|
||||
Set-Location (Split-Path $PSScriptRoot -Parent)
|
||||
|
||||
uv sync --group build
|
||||
uv run pyinstaller --noconfirm --clean packaging/mtga-assistant.spec
|
||||
|
||||
$zip = "dist/MTGA-Assistant-windows.zip"
|
||||
if (Test-Path $zip) { Remove-Item $zip }
|
||||
Compress-Archive -Path "dist/MTGA Assistant" -DestinationPath $zip
|
||||
Write-Host "Built dist\MTGA Assistant\MTGA Assistant.exe and $zip"
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Entry point for the PyInstaller build (PyInstaller needs a script, not a package)."""
|
||||
|
||||
import sys
|
||||
|
||||
from mtga_assistant.app import main
|
||||
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Render assets/icon.svg to assets/icon.ico (PNG-compressed entries at the usual Windows sizes).
|
||||
|
||||
Run with: uv run python packaging/make_icon.py
|
||||
"""
|
||||
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import QBuffer, QByteArray, QIODevice, Qt
|
||||
from PySide6.QtGui import QGuiApplication, QImage, QPainter
|
||||
from PySide6.QtSvg import QSvgRenderer
|
||||
|
||||
SIZES = [16, 24, 32, 48, 64, 128, 256]
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def render_png(renderer: QSvgRenderer, size: int) -> bytes:
|
||||
img = QImage(size, size, QImage.Format.Format_ARGB32)
|
||||
img.fill(Qt.GlobalColor.transparent)
|
||||
painter = QPainter(img)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
renderer.render(painter)
|
||||
painter.end()
|
||||
data = QByteArray()
|
||||
buf = QBuffer(data)
|
||||
buf.open(QIODevice.OpenModeFlag.WriteOnly)
|
||||
img.save(buf, "PNG")
|
||||
return bytes(data)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
QGuiApplication(sys.argv[:1])
|
||||
renderer = QSvgRenderer(str(ROOT / "assets/icon.svg"))
|
||||
pngs = [(size, render_png(renderer, size)) for size in SIZES]
|
||||
# ICONDIR header, then one ICONDIRENTRY per image, then the PNG data.
|
||||
out = struct.pack("<HHH", 0, 1, len(pngs))
|
||||
offset = 6 + 16 * len(pngs)
|
||||
for size, png in pngs:
|
||||
dim = 0 if size >= 256 else size # 0 means 256
|
||||
out += struct.pack("<BBBBHHII", dim, dim, 0, 0, 1, 32, len(png), offset)
|
||||
offset += len(png)
|
||||
out += b"".join(png for _, png in pngs)
|
||||
(ROOT / "assets/icon.ico").write_bytes(out)
|
||||
# The app loads this at runtime (window/taskbar icon), so it lives inside the package.
|
||||
(ROOT / "src/mtga_assistant/icon.png").write_bytes(dict(pngs)[256])
|
||||
print(f"wrote assets/icon.ico ({len(out)} bytes) and src/mtga_assistant/icon.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,42 @@
|
||||
# PyInstaller spec for MTGA Assistant: a one-folder, windowed build.
|
||||
# Build on the target OS (PyInstaller can't cross-compile), e.g. on Windows:
|
||||
# powershell -ExecutionPolicy Bypass -File packaging\build_windows.ps1
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(SPECPATH).parent
|
||||
SRC = ROOT / "src"
|
||||
|
||||
a = Analysis(
|
||||
[str(ROOT / "packaging" / "launch.py")],
|
||||
pathex=[str(SRC)],
|
||||
datas=[(str(SRC / "mtga_assistant" / "icon.png"), "mtga_assistant")],
|
||||
# Qt modules the app doesn't use; keeps the bundle smaller.
|
||||
excludes=[
|
||||
"tkinter",
|
||||
"PySide6.QtNetwork",
|
||||
"PySide6.QtQml",
|
||||
"PySide6.QtQuick",
|
||||
"PySide6.QtSql",
|
||||
"PySide6.QtTest",
|
||||
"PySide6.QtOpenGL",
|
||||
"PySide6.QtPdf",
|
||||
],
|
||||
noarchive=False,
|
||||
)
|
||||
# Not needed by a Qt Widgets app: the software OpenGL fallback (~20 MB) and Qt's translations.
|
||||
a.binaries = [b for b in a.binaries if Path(b[0]).name.lower() != "opengl32sw.dll"]
|
||||
a.datas = [d for d in a.datas if "translations" not in Path(d[0]).parts]
|
||||
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name="MTGA Assistant",
|
||||
console=False,
|
||||
icon=str(ROOT / "assets" / "icon.ico") if sys.platform == "win32" else None,
|
||||
)
|
||||
coll = COLLECT(exe, a.binaries, a.datas, name="MTGA Assistant")
|
||||
Reference in new issue
Block a user