Skip to content

Commit 95deea0

Browse files
committed
feat: add reproducible store screenshots
1 parent d925137 commit 95deea0

11 files changed

Lines changed: 337 additions & 1 deletion

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
88
- EXE neu gebaut 2026-06-01 (PyInstaller, `SoftwareCenter.spec`); 9/9 Tests grün, Smoke-Test bestanden. Vorherige EXE: 2026-05-01.
99

1010
### Changed
11+
- Die Windows-Store-Vorbereitung enthält jetzt ein reproduzierbares Screenshot-Set aus der echten Desktop-Oberfläche statt nur eines offenen TODO-Markers.
1112
- README.md, README_de.md, `llms.txt`, and store listing copy now include sharper discovery positioning for SoftwareCenter as a local-first PySide6 app launcher and desktop shortcut organizer, with explicit disambiguation from Microsoft/SCCM Software Center, app stores, package managers, and remote deployment portals.
1213
- UX-/Accessibility-Review: Der einzige verbleibende Tab zeigt kein irreführendes Schließen-Symbol mehr; die Schaltfläche erscheint erst wieder, wenn wirklich mehr als ein Tab vorhanden ist.
1314
- The Windows Store path now has a reproducible local WACK runner plus a documented manual test protocol instead of an undocumented final certification step.
@@ -25,6 +26,8 @@ All notable changes to this project will be documented in this file.
2526
- GitHub Actions now install the app dependencies and run the PySide6 regression tests on Python 3.10, 3.11, and 3.12.
2627

2728
### Added
29+
- `generate_store_screenshots.py` für reproduzierbare Windows-Store-Screenshots und `README/screenshots/store/README.md` als Generator-Doku.
30+
- Regressionstest `tests/test_store_screenshots.py` für PNG-Header, Mindestauflösung und `summary.json`.
2831
- `llms.txt` in the repository root for LLM documentation visibility.
2932
- Regressionstest dafür, dass die Tab-Schließen-Schaltfläche beim letzten verbleibenden Tab ausgeblendet bleibt und nach dem Hinzufügen weiterer Tabs wieder erscheint.
3033
- `scripts/run_windows_wack.py` for local Windows App Certification Kit dry-runs, real runs, and XML-to-JSON report summaries.

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ python tests/linux_platform_smoke.py
100100
GitHub Actions runs these smoke checks. The macOS smoke validates `.app` import, `open` launching, QSettings persistence, and profile export on `macos-latest`; the Linux smoke covers `.desktop` import, `Exec`/`xdg-open` launching, QSettings persistence, and profile export on `ubuntu-latest`. Build artifacts and local task/test files are ignored and should not be committed.
101101

102102
For the Windows Store path, `python scripts/run_windows_wack.py --dry-run` checks the local MSIX/AppCert paths and prints the reproducible WACK command. The real certification run should be executed from an elevated PowerShell against a fresh signed MSIX before submission.
103+
`python generate_store_screenshots.py` creates the reproducible Store screenshot set under `README/screenshots/store/`.
103104

104105
## Exchange Format
105106

@@ -109,6 +110,12 @@ Profiles can be exported as `softwarecenter-profile-v1.json` and imported again
109110

110111
The [web_companion/README.md](web_companion/README.md) documents a static browser companion for the export format. It imports `softwarecenter-profile-v1.json` locally, offers tab/type filtering, and restores the last loaded profile for offline starts.
111112

113+
## Windows Store Assets
114+
115+
The Windows Store track now includes a reproducible screenshot generator for the
116+
current desktop UI. Run `python generate_store_screenshots.py` to refresh
117+
`README/screenshots/store/` with four redacted Store images and `summary.json`.
118+
112119
## Tech Stack
113120

114121
| Component | Technology |

README/screenshots/store/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Store-Screenshots
2+
3+
Stand: 2026-06-17
4+
5+
## Reproduzierbare Erzeugung
6+
7+
```powershell
8+
python generate_store_screenshots.py
9+
```
10+
11+
Der Generator rendert die echte PySide6-Oberfläche in einem temporären
12+
Arbeitsbereich mit redigierten Demo-Dateien. Private Pfade, echte Programme und
13+
lokale Nutzerinhalte landen dadurch nicht im finalen Store-Set.
14+
15+
## Enthaltenes Screenshot-Set
16+
17+
1. `main-window.png` - Hauptfenster mit lokaler Launcher-Sammlung
18+
2. `tab-organization.png` - mehrere Workflow-Tabs im selben Profil
19+
3. `tiles-view.png` - Kachelansicht für schnellen Zugriff
20+
4. `list-view.png` - Listenansicht für größere Sammlungen
21+
5. `summary.json` - kompakte Inventarliste der erzeugten Artefakte
22+
23+
## Qualitätsregeln
24+
25+
- Keine privaten Dateinamen, realen Installationspfade oder Nutzerinhalte im Store-Set
26+
- Einheitliche Fenstergröße und klare, helle Lesbarkeit
27+
- Keine Build-Artefakte oder temporären Testordner sichtbar
17.1 KB
Loading
17.5 KB
Loading
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"generated_at": "2026-06-17T13:14:31Z",
3+
"screenshots": [
4+
{
5+
"name": "main",
6+
"file": "main-window.png",
7+
"caption": "Hauptfenster mit lokalen Launcher-Kacheln"
8+
},
9+
{
10+
"name": "tabs",
11+
"file": "tab-organization.png",
12+
"caption": "Mehrere Workflow-Tabs im selben Profil"
13+
},
14+
{
15+
"name": "tiles",
16+
"file": "tiles-view.png",
17+
"caption": "Kachelansicht für schnellen Zugriff"
18+
},
19+
{
20+
"name": "list",
21+
"file": "list-view.png",
22+
"caption": "Listenansicht für größere Sammlungen"
23+
}
24+
]
25+
}
13.2 KB
Loading
17.1 KB
Loading

STORE_LISTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ Productivity / Utilities
120120
- [x] No debug print statements
121121
- [x] PySide6 (LGPL) -- license compatible with Store distribution
122122
- [x] README-Screenshot vorhanden (`README/screenshots/main.png`)
123-
- [ ] Store-Screenshot-Set erstellen: mindestens 3-4 Bilder in Store-Auflösung (z. B. Hauptfenster, Tab-Organisation, Kachelansicht, Listenansicht)
123+
- [x] Store-Screenshot-Set erzeugbar: `python generate_store_screenshots.py` schreibt `README/screenshots/store/main-window.png`, `tab-organization.png`, `tiles-view.png`, `list-view.png` und `summary.json`
124124
- [x] MSIX package vorhanden (`releases/SoftwareCenter.msix`, Stand 2026-03-13)
125125
- [ ] Code-Signing (.pfx) -- noch nicht konfiguriert in store_settings.json
126126
- [x] WACK-Workflow und manuelles Store-Testprotokoll sind dokumentiert

generate_store_screenshots.py

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import os
5+
import tempfile
6+
import time
7+
from pathlib import Path
8+
9+
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
10+
os.environ.setdefault("QT_SCALE_FACTOR", "1")
11+
12+
from PySide6 import QtCore, QtGui, QtWidgets
13+
14+
import SoftwareCenter as module
15+
from SoftwareCenter import MainWindow
16+
17+
18+
PROJECT_ROOT = Path(__file__).resolve().parent
19+
OUTPUT_DIR = PROJECT_ROOT / "README" / "screenshots" / "store"
20+
21+
SCREENSHOT_FILES = {
22+
"main": "main-window.png",
23+
"tabs": "tab-organization.png",
24+
"tiles": "tiles-view.png",
25+
"list": "list-view.png",
26+
}
27+
28+
29+
def _process_events(app: QtWidgets.QApplication, duration: float = 0.08) -> None:
30+
deadline = time.monotonic() + duration
31+
while time.monotonic() < deadline:
32+
app.processEvents()
33+
time.sleep(0.01)
34+
35+
36+
def _configure_runtime_dirs(temp_root: Path) -> Path:
37+
home_dir = temp_root / "home"
38+
home_dir.mkdir(parents=True, exist_ok=True)
39+
40+
os.environ["HOME"] = str(home_dir)
41+
os.environ["USERPROFILE"] = str(home_dir)
42+
os.environ["APPDATA"] = str(home_dir / "AppData" / "Roaming")
43+
os.environ["LOCALAPPDATA"] = str(home_dir / "AppData" / "Local")
44+
os.environ["XDG_CONFIG_HOME"] = str(home_dir / ".config")
45+
46+
settings_root = temp_root / "qsettings"
47+
settings_root.mkdir(parents=True, exist_ok=True)
48+
QtCore.QSettings.setDefaultFormat(QtCore.QSettings.IniFormat)
49+
QtCore.QSettings.setPath(
50+
QtCore.QSettings.IniFormat,
51+
QtCore.QSettings.UserScope,
52+
str(settings_root),
53+
)
54+
QtCore.QSettings.setPath(
55+
QtCore.QSettings.IniFormat,
56+
QtCore.QSettings.SystemScope,
57+
str(settings_root),
58+
)
59+
return home_dir
60+
61+
62+
def _write_demo_targets(workspace: Path) -> dict[str, Path]:
63+
workspace.mkdir(parents=True, exist_ok=True)
64+
65+
targets = {
66+
"briefing": workspace / "Projekt-Briefing.txt",
67+
"editor": workspace / "Redaktion.py",
68+
"mail": workspace / "Mail-Export.cmd",
69+
"ocr": workspace / "PDF-Werkstatt.bat",
70+
"research": workspace / "Recherche.url",
71+
"sync": workspace / "Sync-Ordner.lnk",
72+
"release": workspace / "Release-Check.ps1",
73+
"notes": workspace / "Notizen.md",
74+
}
75+
demo_text = {
76+
"briefing": "Projekt-Briefing für lokale Software-Sammlungen.\n",
77+
"editor": "print('Lokale Redaktion mit echten Umlauten: äöü')\n",
78+
"mail": "@echo off\r\necho Mail-Export vorbereiten\r\n",
79+
"ocr": "@echo off\r\necho PDF-Werkstatt starten\r\n",
80+
"research": "[InternetShortcut]\nURL=https://example.invalid/recherche\n",
81+
"sync": "Demo-Verknüpfung für lokale Synchronisation\n",
82+
"release": "Write-Output 'Release-Check vorbereiten'\n",
83+
"notes": "# Notizen\n\nDesktop-Profile bleiben lokal.\n",
84+
}
85+
for key, path in targets.items():
86+
path.write_text(demo_text[key], encoding="utf-8")
87+
return targets
88+
89+
90+
def _entry(path: Path, label: str, kind: str, notes: str | None = None) -> dict[str, str | None]:
91+
return {
92+
"path": str(path),
93+
"label": label,
94+
"kind": kind,
95+
"notes": notes,
96+
}
97+
98+
99+
def _apply_consistent_icons(list_widget: module.SoftwareListWidget, icon: QtGui.QIcon) -> None:
100+
for index in range(list_widget.count()):
101+
item = list_widget.item(index)
102+
item.setIcon(icon)
103+
104+
105+
def _configure_demo_window(window: MainWindow, targets: dict[str, Path]) -> None:
106+
default_page = window.current_page()
107+
if default_page is None:
108+
raise RuntimeError("SoftwareCenter konnte keine Startseite erzeugen")
109+
110+
workspace_entries = [
111+
_entry(targets["briefing"], "Projekt-Briefing", "file", "Startpunkt für lokale Arbeitsplätze"),
112+
_entry(targets["editor"], "Redaktion", "script", "Bearbeitung und Review"),
113+
_entry(targets["mail"], "Mail-Export", "script", "Regelbasierte Übergabe"),
114+
_entry(targets["ocr"], "PDF-Werkstatt", "script", "OCR und PDF-Werkzeuge"),
115+
_entry(targets["research"], "Recherche", "url", "Schneller Sprung zu Referenzen"),
116+
_entry(targets["sync"], "Sync-Ordner", "windows_shortcut", "Lokale Zielpfade im Blick"),
117+
]
118+
office_entries = [
119+
_entry(targets["notes"], "Projekt-Notizen", "file", "Besprechungen und offene Punkte"),
120+
_entry(targets["briefing"], "Freigabe-Briefing", "file", "Kompakte Zusammenfassung"),
121+
_entry(targets["release"], "Release-Check", "script", "Vor dem Versand prüfen"),
122+
]
123+
review_entries = [
124+
_entry(targets["editor"], "Text-Review", "script", "Korrektur und Endabnahme"),
125+
_entry(targets["mail"], "Mail-Status", "script", "Letzten Export kontrollieren"),
126+
_entry(targets["ocr"], "Dokumente", "script", "Neue PDFs prüfen"),
127+
]
128+
129+
default_page.add_entries(workspace_entries)
130+
window.tabs.setTabText(0, "Arbeitsplatz")
131+
window.add_new_tab("Office", "tiles", entries=office_entries)
132+
window.add_new_tab("Review", "list", entries=review_entries)
133+
window.add_new_tab(
134+
"Setup",
135+
"tiles",
136+
entries=[
137+
_entry(targets["sync"], "Zielpfade", "windows_shortcut", "Nur Referenz, kein Upload"),
138+
_entry(targets["release"], "Versionen", "script", "Checks für die Übergabe"),
139+
],
140+
)
141+
142+
app_icon = window.windowIcon()
143+
if app_icon.isNull():
144+
app_icon = QtGui.QIcon(str(PROJECT_ROOT / "icon.ico"))
145+
for index in range(window.tabs.count()):
146+
page = window.tabs.widget(index)
147+
if isinstance(page, module.TabPage):
148+
_apply_consistent_icons(page.list, app_icon)
149+
150+
151+
def _save_widget(widget: QtWidgets.QWidget, target: Path) -> None:
152+
widget.show()
153+
widget.raise_()
154+
widget.activateWindow()
155+
app = QtWidgets.QApplication.instance()
156+
if app is not None:
157+
_process_events(app)
158+
pixmap = widget.grab()
159+
if pixmap.isNull():
160+
raise RuntimeError(f"Screenshot für {target.name} konnte nicht erzeugt werden")
161+
target.parent.mkdir(parents=True, exist_ok=True)
162+
if not pixmap.save(str(target)):
163+
raise RuntimeError(f"Screenshot {target} konnte nicht gespeichert werden")
164+
165+
166+
def _write_summary(targets: list[Path]) -> Path:
167+
summary_path = targets[0].parent / "summary.json"
168+
payload = {
169+
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
170+
"screenshots": [
171+
{"name": "main", "file": SCREENSHOT_FILES["main"], "caption": "Hauptfenster mit lokalen Launcher-Kacheln"},
172+
{"name": "tabs", "file": SCREENSHOT_FILES["tabs"], "caption": "Mehrere Workflow-Tabs im selben Profil"},
173+
{"name": "tiles", "file": SCREENSHOT_FILES["tiles"], "caption": "Kachelansicht für schnellen Zugriff"},
174+
{"name": "list", "file": SCREENSHOT_FILES["list"], "caption": "Listenansicht für größere Sammlungen"},
175+
],
176+
}
177+
summary_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
178+
return summary_path
179+
180+
181+
def generate_store_screenshots(output_dir: Path) -> list[Path]:
182+
output_dir = Path(output_dir)
183+
output_dir.mkdir(parents=True, exist_ok=True)
184+
185+
with tempfile.TemporaryDirectory(prefix="softwarecenter-store-shots-") as temp_dir:
186+
temp_root = Path(temp_dir)
187+
home_dir = _configure_runtime_dirs(temp_root)
188+
targets = _write_demo_targets(home_dir / "workspace")
189+
190+
QtCore.QStandardPaths.setTestModeEnabled(True)
191+
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication([])
192+
app.setApplicationName("SoftwareCenter Store Screenshots")
193+
app.setQuitOnLastWindowClosed(False)
194+
195+
settings = QtCore.QSettings(str(temp_root / "softwarecenter.ini"), QtCore.QSettings.IniFormat)
196+
window = MainWindow(settings=settings)
197+
window.resize(1600, 960)
198+
_configure_demo_window(window, targets)
199+
200+
result_paths = [
201+
output_dir / SCREENSHOT_FILES["main"],
202+
output_dir / SCREENSHOT_FILES["tabs"],
203+
output_dir / SCREENSHOT_FILES["tiles"],
204+
output_dir / SCREENSHOT_FILES["list"],
205+
]
206+
207+
try:
208+
window.tabs.setCurrentIndex(0)
209+
window.set_current_view("tiles")
210+
window.statusBar().showMessage("Lokale Sammlung · 6 Einträge · Offline", 0)
211+
_save_widget(window, result_paths[0])
212+
213+
window.tabs.setCurrentIndex(2)
214+
window.statusBar().showMessage("Tabs für Arbeitsplatz, Office, Review und Setup", 0)
215+
_save_widget(window, result_paths[1])
216+
217+
window.tabs.setCurrentIndex(0)
218+
window.set_current_view("tiles")
219+
window.statusBar().showMessage("Kachelansicht für schnellen Programmzugriff", 0)
220+
_save_widget(window, result_paths[2])
221+
222+
window.set_current_view("list")
223+
window.statusBar().showMessage("Listenansicht für größere Sammlungen", 0)
224+
_save_widget(window, result_paths[3])
225+
finally:
226+
window.close()
227+
_process_events(app)
228+
229+
_write_summary(result_paths)
230+
return result_paths
231+
232+
233+
def main() -> None:
234+
targets = generate_store_screenshots(OUTPUT_DIR)
235+
for target in targets:
236+
print(target.name)
237+
238+
239+
if __name__ == "__main__":
240+
main()

0 commit comments

Comments
 (0)