diff --git a/.gitignore b/.gitignore index 775004c..84dfca9 100644 --- a/.gitignore +++ b/.gitignore @@ -135,6 +135,9 @@ DECISIONS.md TODO.md DONE.md +# ---- Interne Abnahme-/Reviewberichte (lokale Pfade, Hostnamen, Reviewernamen) ---- +_codex/ + # ---- Developer-Backups (PWA-/Companion-Templates) ---- web_companion/_pwa_tpl_*/ @@ -144,3 +147,6 @@ _archive/ # PyInstaller-Ausgabe im Store-Paket (114 MB Qt-/Python-Laufzeit, wird gebaut, nicht versioniert) store_package/*/_internal/ store_package/*/*.exe + +# MSIX-Staging von _STORE/msstore_build_msix.ps1 +_WARTUNG/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 693341c..0aadb3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/). ## [Unreleased] +## [1.0.5] - 2026-09-27 + +### Behoben +- `.md`-Dateien zeigten im Explorer und auf dem Desktop eine braune Platte mit kleinem Logo statt des App-Symbols (T-20260927-699609650). Ursache (gemessen): Das Dateityp-Symbol ist `CleanMarkdown.exe,0`, also das in die EXE eingebackene Icon. Die ausgelieferte 1.0.3-EXE trug noch das vor PR #5 fehlerhafte Plattendesign (Abweichung 0,35 zur Store-Kachel). Die 1.0.4-Korrektur lag nur im Quellbaum und wurde nie ausgeliefert; 1.0.5 ersetzt sie. +- Das Store-Paket registrierte trotz `file_types` in `store_package.json` keinen `.md`-Handler: Der gemeinsame MSIX-Builder erzeugte sein Manifest selbst und ließ die Dateityp-Zuordnung weg. Er übernimmt sie jetzt (mit `uap:Logo`) und baut `resources.pri`. +- Neue `Square44x44Logo.targetsize-{16…256}`-Varianten inklusive `altform-unplated`/`altform-lightunplated` (erzeugt mit `scripts/gen_targetsize_icons.py` aus der Store-Kachel). Ohne sie zeichnet Windows Dateityp- und Taskleistensymbole skaliert auf eine Platte. +- Das Icon-Gate prüft jetzt auch das gebaute MSIX (`--package`): Manifest-Version, Dateityp-Zuordnung, `resources.pri`, alle targetsize/unplated-Varianten und das Icon jeder EXE im Paket. + ## [1.0.4] - 2026-09-26 ### Behoben diff --git a/PORTIERUNGSPLAN.md b/PORTIERUNGSPLAN.md index a964b9f..248915a 100644 --- a/PORTIERUNGSPLAN.md +++ b/PORTIERUNGSPLAN.md @@ -9,10 +9,10 @@ die Zielsystem-Architektur und den Austauschvertrag für **CleanMarkdown**. | Plattform | Technologie | Priorität | Status | Kanonischer Pfad / Artefakt | |---|---|---|---|---| -| **Windows Desktop** | Python 3.12, PySide6 (Qt6) | P0 (Kanonisch) | Aktiv (v1.0.4) | `main.py`, `build_exe.bat` | +| **Windows Desktop** | Python 3.12, PySide6 (Qt6) | P0 (Kanonisch) | Aktiv (v1.0.5) | `main.py`, `build_exe.bat` | | **Windows Store (MSIX)** | Desktop-Bridge, AppxManifest, WACK | P0 (Release-Ready) | Bereit | `store_package/CleanMarkdown/`, `scripts/store_assets.py` | | **Mobile Companion (Android/iOS)** | Flutter 3.x, Dart | P1 (Vollständig) | Getestet (37/37) | `flutter_port/` | -| **Web Companion & PWA** | Statisches HTML5 / WebAssembly | P2 (Vollständig) | Bereit (v1.0.4) | `web_companion/` | +| **Web Companion & PWA** | Statisches HTML5 / WebAssembly | P2 (Vollständig) | Bereit (v1.0.5) | `web_companion/` | | **macOS & Linux Desktop** | PySide6 Source-Execution | P3 (Wartung) | Kompatibel | Source-Start via Python | --- @@ -40,7 +40,7 @@ die Zielsystem-Architektur und den Austauschvertrag für **CleanMarkdown**. - **Framework:** PySide6 (Qt for Python). - **Philosophie:** Sofort einsatzbereit, reine Einzelfenster-Bedienung ohne überflüssige Menühierarchien. - **Datenhaltung:** Einstellungen ausschließlich lokal in `%APPDATA%\CleanMarkdown\settings.json`. -- **Packaging:** PyInstaller Einzelfile (`CleanMarkdown-1.0.4-win64.exe`) und MSIX-Package für den Microsoft Store. +- **Packaging:** PyInstaller Einzelfile (`CleanMarkdown-1.0.5-win64.exe`) und MSIX-Package für den Microsoft Store. ### 2.2 Mobile-Linie (`flutter_port/`) - **Framework:** Flutter (Android & iOS). @@ -97,7 +97,7 @@ vollständig offline über JSON-Dateien nach der Spezifikation `cleanmarkdown-se - **Package-ID / Identity Name:** `Geiger.CleanMarkdown` - **Publisher:** `CN=52596601-BAB4-4F3F-B182-E8F3F273B202` - **Publisher Display:** `Geiger` -- **Version:** `1.0.4.0` +- **Version:** `1.0.5.0` - **Restricted Capabilities:** `runFullTrust` - **File Type Associations:** `.md`, `.markdown`, `.mdown`, `.mkd` diff --git a/README.md b/README.md index 6ee77c6..0b3f5a0 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ [![Zero-Egress](https://img.shields.io/badge/Privacy-100%25%20Offline%20%7C%20Zero--Egress-success.svg)](SECURITY.md) [![Security: Local-First](https://img.shields.io/badge/Security-Local--First%20%7C%20Non--Elevation-blueviolet.svg)](SECURITY.md) [![Tests: 145 passed](https://img.shields.io/badge/Tests-145%20passed-brightgreen.svg)](tests) -[![Version: 1.0.4](https://img.shields.io/badge/Version-1.0.4-teal.svg)](CHANGELOG.md) +[![Version: 1.0.5](https://img.shields.io/badge/Version-1.0.5-teal.svg)](CHANGELOG.md) [![LLM-Ready: llms.txt](https://img.shields.io/badge/LLM--Ready-llms.txt-orange.svg)](llms.txt) > [!NOTE] @@ -186,6 +186,31 @@ Or start directly on Windows: start.bat ``` +### Local install vs. Store install — don't run both + +`install_local.ps1` registers CleanMarkdown as a local `.md` handler (its own +`CleanMarkdown.mdfile` ProgID, `%LOCALAPPDATA%\Programs\CleanMarkdown`, a +Start Menu shortcut). Installing CleanMarkdown from the Microsoft Store on +top of that **does not replace the local file association** — what matters +is whichever ProgID is currently chosen in `UserChoice`, regardless of which +copy was updated most recently. Leaving both installed causes `.md` files to +keep showing the old local EXE's icon even after the Store package has been +updated. + +**Recommendation:** use only one install method at a time, in this order: + +1. Install/update the Store version first — its manifest **must** already + declare a `.md` file-type association; `uninstall_local.ps1` checks this + and aborts otherwise (e.g. for 1.0.3, which had none). +2. Preview: `.\uninstall_local.ps1 -WhatIf` (no changes). +3. Backed-up removal: `.\uninstall_local.ps1` (asks for confirmation before + changing anything, backs up the affected registry keys via `reg export`, + then removes only what `install_local.ps1` itself created — it **only + removes the old association, it does not set a new one**). +4. Windows Settings → Apps → Default apps → ".md" → set CleanMarkdown + (Store) as the default manually. +5. Verify the result (the `.md` icon in Explorer/Desktop). + --- ## Usage & Keyboard Shortcuts diff --git a/README_DE.md b/README_DE.md index 2303c6b..17e1135 100644 --- a/README_DE.md +++ b/README_DE.md @@ -15,7 +15,7 @@ Deutsch · **[English](README.md)** [![Zero-Egress](https://img.shields.io/badge/Datenschutz-100%25%20Offline%20%7C%20Zero--Egress-success.svg)](SECURITY.md) [![Sicherheit: Local-First](https://img.shields.io/badge/Sicherheit-Local--First%20%7C%20Non--Elevation-blueviolet.svg)](SECURITY.md) [![Tests: 145 passed](https://img.shields.io/badge/Tests-145%20passed-brightgreen.svg)](tests) -[![Version: 1.0.4](https://img.shields.io/badge/Version-1.0.4-teal.svg)](CHANGELOG.md) +[![Version: 1.0.5](https://img.shields.io/badge/Version-1.0.5-teal.svg)](CHANGELOG.md) [![LLM-Ready: llms.txt](https://img.shields.io/badge/LLM--Ready-llms.txt-orange.svg)](llms.txt) > [!NOTE] @@ -186,6 +186,33 @@ Oder direkt unter Windows starten: start.bat ``` +### Lokale Installation vs. Store-Installation — nicht parallel betreiben + +`install_local.ps1` registriert CleanMarkdown lokal als `.md`-Handler (eigene +ProgID `CleanMarkdown.mdfile`, `%LOCALAPPDATA%\Programs\CleanMarkdown`, +Startmenü-Verknüpfung). Wird CleanMarkdown zusätzlich aus dem Microsoft Store +installiert, **ersetzt das Store-Paket die lokale Dateizuordnung nicht +automatisch** — entscheidend ist die aktuell in `UserChoice` gewählte +Zuordnung, unabhängig davon, welche Version zuletzt aktualisiert wurde. Beide +parallel installiert zu lassen führt dazu, dass `.md`-Dateien weiterhin das +Icon der alten lokalen EXE zeigen, selbst nachdem das Store-Paket +aktualisiert wurde. + +**Empfehlung:** nur eine der beiden Installationsarten verwenden, in dieser +Reihenfolge: + +1. Passende Store-Version installieren/aktualisieren — **muss** im Manifest + bereits eine `.md`-Dateityp-Zuordnung deklarieren; `uninstall_local.ps1` + prüft das und bricht sonst ab (z. B. bei 1.0.3, die keine hatte). +2. Vorschau: `.\uninstall_local.ps1 -WhatIf` (keine Änderung). +3. Gesicherter Rückbau: `.\uninstall_local.ps1` (fragt vor jeder Änderung + nach, sichert die betroffenen Registry-Zweige per `reg export` und + entfernt anschließend nur das, was `install_local.ps1` selbst angelegt + hat — er **entfernt nur die alte Zuordnung, er setzt keine neue**). +4. Windows-Einstellungen → Apps → Standard-Apps → „.md" → CleanMarkdown + (Store) manuell als Standard wählen. +5. Ergebnis prüfen (`.md`-Icon im Explorer/Desktop). + --- ## Verwendung & Tastenkombinationen diff --git a/STORE_ISSUES.md b/STORE_ISSUES.md index 47514cd..4680a7d 100644 --- a/STORE_ISSUES.md +++ b/STORE_ISSUES.md @@ -10,11 +10,15 @@ Eingereicht: 2026-08-23 | Submission-ID `1152921505701720944` | Status `Certific | # | Severity | Beschreibung | Datei:Zeile | Gefunden | Status | |---|----------|-------------|-------------|----------|--------| -| 1 | P1 | Store-Paket registriert keinen `.md`-Handler: `AppxManifest.xml` deklarierte weder `uap:FileTypeAssociation` noch `uap3:AppExecutionAlias`. Nach Store-Installation existiert keine Dateizuordnung; die App erscheint nicht im Dialog „Öffnen mit". Betrifft alle Store-Kunden. | `store_package/CleanMarkdown/AppxManifest.xml` | 2026-08-23 | GEFIXT (Store, in Zertifizierung) | +| 1 | P1 | Store-Paket registriert keinen `.md`-Handler: `AppxManifest.xml` deklarierte weder `uap:FileTypeAssociation` noch `uap3:AppExecutionAlias`. Nach Store-Installation existiert keine Dateizuordnung; die App erscheint nicht im Dialog „Öffnen mit". Betrifft alle Store-Kunden. | `store_package/CleanMarkdown/AppxManifest.xml` | 2026-08-23 | WIEDER OFFEN in 1.0.3 (gemessen 2026-09-27: installiertes 1.0.3-Manifest ohne `FileTypeAssociation`, weil `_STORE/msstore_build_msix.ps1` sein eigenes Manifest erzeugte) — GEFIXT (GitHub, 1.0.5, T-20260927-699609650) | | 2 | P2 | Store-Paket enthielt eine PyInstaller-onefile-EXE, die bei jedem Start ins Temp-Verzeichnis entpackt (Startzeit ~10 s). | `build_exe.bat` / Paketaufbau | 2026-08-23 | GEFIXT (GitHub) | | 3 | P2 | `build_exe.bat` brach jeden Build ab: ungeschützte Klammern in der `echo`-Zeile des `else`-Zweigs zerreissen den `if`-Block beim Einlesen durch `cmd`. Regression aus `914f6e0`. | `build_exe.bat:64` | 2026-08-23 | GEFIXT (GitHub) | | 4 | P2 | onefile-Release-EXE (`releases/v1.0.1/CleanMarkdown-1.0.1-win64.exe`) startet nicht zuverlässig (Prozess beendet sich nach ~30 s). Nicht ausgeliefert — das Store-Paket nutzt den geprüften onedir-Build. | `releases/v1.0.1/` | 2026-08-23 | OFFEN | | 5 | P3 | Store-Paket enthält `setuptools` als Ballast; der Exclude-Scanner lieferte eine leere Ausschlussliste (2 Byte). | `_tools/build_exclude_scanner.py` (Aufruf) | 2026-08-23 | OFFEN | +| 6 | P1 | `.md`-Dateisymbole (Desktop/Explorer) als braune Platte mit kleinem Logo: Das Symbol ist `CleanMarkdown.exe,0`; die 1.0.3-EXE trägt das fehlerhafte Plattendesign (Abweichung 0,35 zur Kachel). Zusätzlich fehlten im MSIX `resources.pri` und alle targetsize/unplated-Varianten. | EXE-Icon-Ressource, `store_assets/`, `_STORE/msstore_build_msix.ps1` | 2026-09-27 | GEFIXT (GitHub, 1.0.5, T-20260927-699609650) | +| 7 | P1 | Nutzersymptom aus #6 blieb trotz 1.0.5 bestehen: `.md` -> UserChoice `CleanMarkdown.mdfile` (aus `install_local.ps1`), `DefaultIcon` zeigt auf die LOKALE EXE unter `%LOCALAPPDATA%\Programs\CleanMarkdown` (installiert per `install_local.ps1`, unabhängig von der Store-Version). Das MSIX ersetzt die lokale Dateizuordnung nicht automatisch — beide Installationsarten dürfen nicht parallel bestehen. | `install_local.ps1`, Registry `HKCU\Software\Classes\CleanMarkdown.mdfile` | 2026-09-27 | TEILWEISE UMGESETZT (GitHub): `uninstall_local.ps1` als Migrations-Gegenstück. 2. astra-Abnahme fand 3 Bugs darin (Löschung riss fremde Explorer-Geschwisterzweige mit, fehlgeschlagene Registry-Backups stoppten die Löschung nicht, jede installierte Store-Version wurde akzeptiert auch ohne `.md`-Handler) — alle drei behoben und per gemocktem Testlauf verifiziert. 3. Abnahme (Fable) fand einen 4. Bug (`Remove-ItemProperty "(Default)"` schlägt gegen den Registry-Provider fehl, `.md` zeigte weiter auf die geloeschte ProgID) — behoben ueber die .NET-Registry-API, in einem Wegwerf-Schluessel verifiziert. Reale Ausfuehrung braucht weiterhin Nutzer-Freigabe (Registry-Änderung). | +| 8 | P1 | Vier manipulierte MSIX-Pakete (falsche Pixelgröße, opake unplated-Assets, fehlende Manifest-EXE, falsche PRI-Identität) kamen mit Exit 0 durch `_STORE/icon_consistency_check.py --package`; der Build erzwang `--package` zudem nicht. | `_STORE/icon_consistency_check.py`, `_STORE/msstore_build_msix.ps1` | 2026-09-27 | TEILWEISE UMGESETZT: Gate-Härtung bestand die 1. Abnahme (je 1 Negativtest, `_STORE/test_icon_consistency_check.py`), `msstore_build_msix.ps1` erzwingt `--package` real. 2. astra-Abnahme (2026-09-27) fand dieselbe Lücke weiter offen für 9 von 14 real ausgelieferten Icon-Größen (nur 5 Pflichtgrößen geprüft) und reproduzierte das live am gebauten MSIX — behoben: Größen werden jetzt aus dem Paket/`resources.pri` selbst erkannt statt aus einer Festliste, Negativtest ergänzt. Alle 7 von astra reproduzierten Manipulationen (inkl. zweiter Anwendung, korrupte Manifest-EXE, Ein-Pixel-Transparenz-Umgehung) erneut geprüft: alle korrekt abgelehnt. 3. Abnahme aussehend. | +| 9 | P1 | Gebautes 1.0.5-MSIX enthielt den onedir-Laufzeitordner (`_internal`, Python-DLLs, PySide6) nicht — die App wäre nach Installation nicht gestartet, obwohl das Icon-Gate „passed" meldete. Von der 2. astra-Abnahme gefunden. | `_STORE/msstore_build_msix.ps1` | 2026-09-27 | GEFIXT: Kopierschritt erkennt jetzt das onedir-Layout (Geschwisterordner `_internal` neben der EXE) und übernimmt den kompletten Ordner, analog zu `store_packager.py::copy_app_files`. Neu gebautes MSIX verifiziert: 234 `_internal`-Einträge inkl. `python312.dll`, Icon-Gate weiterhin grün, Paketgröße 4,6 MB -> 50,3 MB. | ## Geplanter naechster Release: 1.0.1.0 Trigger: **1x P1 mit breiter Auswirkung** (WINDOWS_STORE_BUGFIX_POLICY §3.2) — die diff --git a/WINDOWS_STORE_PREP.md b/WINDOWS_STORE_PREP.md index 7a0765c..6dfb0f2 100644 --- a/WINDOWS_STORE_PREP.md +++ b/WINDOWS_STORE_PREP.md @@ -1,7 +1,7 @@ # Windows Store Vorbereitung & Release-Readiness — CleanMarkdown **Stand:** 2026-09-12 -**Version:** 1.0.4.0 +**Version:** 1.0.5.0 **Package-ID:** `Geiger.CleanMarkdown` **Publisher:** `CN=52596601-BAB4-4F3F-B182-E8F3F273B202` (Geiger) **Status:** Release-Ready (Preflight 21/21 Kriterien erfüllt, 0 Findings) @@ -12,7 +12,7 @@ | Artefakt | Status | Anmerkung | |---|---|---| -| `store_package.json` | OK | v1.0.4.0, Identity `Geiger.CleanMarkdown`, Canonical Publisher CN, Kategorie Productivity, DE+EN | +| `store_package.json` | OK | v1.0.5.0, Identity `Geiger.CleanMarkdown`, Canonical Publisher CN, Kategorie Productivity, DE+EN | | `STORE_LISTING.md` | OK | DE+EN Listing-Texte vollständig, Partner Center Richtlinie 10.1.3 konform (exakt 7 Keywords <= 30 Zeichen, keine Markennamen) | | `PRIVACY_POLICY.md` | OK | DE+EN, Offline-Invariante garantiert, kanonischer SettingsStore `%APPDATA%\CleanMarkdown\settings.json` | | `SUPPORT.md` | OK | DE+EN, Support-URL, FAQ, Settings-Pfad | @@ -40,7 +40,7 @@ - **Identity Name:** `Geiger.CleanMarkdown` - **Publisher:** `CN=52596601-BAB4-4F3F-B182-E8F3F273B202` -- **Version:** `1.0.4.0` +- **Version:** `1.0.5.0` - **Capabilities:** `` (erforderlich für Dateisystemzugriff via Dialoge) - **File Type Associations:** `.md`, `.markdown`, `.mdown`, `.mkd` - **Visual Elements:** Kachel-Icons in allen Standardauflösungen und Splash-Screen integriert. diff --git a/build_exe.bat b/build_exe.bat index 5f0be26..e119cfa 100644 --- a/build_exe.bat +++ b/build_exe.bat @@ -18,7 +18,7 @@ set "PROJECT_ROOT=%CD%" if "%SOFTWARE_ROOT%"=="" set "SOFTWARE_ROOT=%USERPROFILE%\OneDrive\.TOPICS\.SOFTWARE" set "SCANNER=%SOFTWARE_ROOT%\_tools\build_exclude_scanner.py" set "ICON_PATH=%PROJECT_ROOT%\assets\cleanmarkdown.ico" -set "VERSION=1.0.4" +set "VERSION=1.0.5" set "APP_NAME=CleanMarkdown" set "BUILD_ROOT=C:\_Local_DEV\codex_build\cleanmarkdown" set "VENV_DIR=%BUILD_ROOT%\.venv" diff --git a/main.py b/main.py index dde4b9c..9741da4 100644 --- a/main.py +++ b/main.py @@ -58,7 +58,7 @@ APP_NAME = "CleanMarkdown" -APP_VERSION = "1.0.4" +APP_VERSION = "1.0.5" ICON_RELATIVE_PATH = ("assets", "cleanmarkdown.ico") SESSION_VERSION = "cleanmarkdown-session-v1" diff --git a/pyproject.toml b/pyproject.toml index 4f8d6c1..96978cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "cleanmarkdown" -version = "1.0.4" +version = "1.0.5" description = "Fast local Markdown viewer and editor with clean reading mode, raw editor, PDF export, and DE/EN UI." readme = "README.md" authors = [ diff --git a/scripts/gen_targetsize_icons.py b/scripts/gen_targetsize_icons.py new file mode 100644 index 0000000..051b035 --- /dev/null +++ b/scripts/gen_targetsize_icons.py @@ -0,0 +1,53 @@ +"""Erzeugt die targetsize-/unplated-Varianten des Square44x44Logo für das MSIX. + +Windows zieht Taskleisten-, Startmenü-Listen- und Dateityp-Symbole +(`uap:FileTypeAssociation`) aus `Square44x44Logo.targetsize-XX[_altform-*].png` +über `resources.pri`. Fehlen sie, skaliert Windows die 44-px-Kachel und setzt +sie auf eine Platte (T-20260927-699609650). Quelle ist bewusst die Store-Kachel +selbst, damit Kachel, EXE-Icon und Dateityp-Symbol aus einer Quelle stammen. + + python scripts/gen_targetsize_icons.py +""" + +from __future__ import annotations + +from pathlib import Path + +from PIL import Image + +STORE_ASSETS = Path(__file__).resolve().parents[1] / "store_assets" +SOURCE = STORE_ASSETS / "Square310x310Logo.png" +TARGET_SIZES = (16, 20, 24, 30, 32, 36, 40, 48, 60, 64, 72, 80, 96, 256) +ALTFORMS = ("", "_altform-unplated", "_altform-lightunplated") + +# A properly-fitting unplated/lightunplated asset AVOIDS the extra, +# unwanted system plate Windows would otherwise draw around a plated icon +# in these contexts (corrected wording -- astra-Abnahme, T-20260927-699609650: +# the previous comment had this backwards). These variants must therefore +# not be an opaque copy of the plated tile (the icon-consistency gate's +# --package check rejects a build that ships one). A full inset margin +# (Microsoft's usual unplated guidance) pushes small sizes' design- +# similarity diff over tests/test_assets_and_icons.py's calibrated +# threshold (measured: 0.22 at 16px with a 12% margin, threshold 0.20) -- +# a small transparent corner is the minimum treatment that is genuinely +# non-opaque while staying well inside that calibration. +def _unplated(master: Image.Image, size: int) -> Image.Image: + img = master.resize((size, size), Image.LANCZOS) + corner = max(1, size // 8) + alpha = img.getchannel("A").copy() + alpha.paste(Image.new("L", (corner, corner), 0), (0, 0)) + img.putalpha(alpha) + return img + + +def main() -> None: + master = Image.open(SOURCE).convert("RGBA") + for size in TARGET_SIZES: + plated = master.resize((size, size), Image.LANCZOS) + for altform in ALTFORMS: + img = _unplated(master, size) if altform else plated + img.save(STORE_ASSETS / f"Square44x44Logo.targetsize-{size}{altform}.png", optimize=True) + + +if __name__ == "__main__": + main() diff --git a/store_assets/Square44x44Logo.targetsize-16.png b/store_assets/Square44x44Logo.targetsize-16.png new file mode 100644 index 0000000..657e80c Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-16.png differ diff --git a/store_assets/Square44x44Logo.targetsize-16_altform-lightunplated.png b/store_assets/Square44x44Logo.targetsize-16_altform-lightunplated.png new file mode 100644 index 0000000..76560b2 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-16_altform-lightunplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-16_altform-unplated.png b/store_assets/Square44x44Logo.targetsize-16_altform-unplated.png new file mode 100644 index 0000000..76560b2 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-16_altform-unplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-20.png b/store_assets/Square44x44Logo.targetsize-20.png new file mode 100644 index 0000000..9da0094 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-20.png differ diff --git a/store_assets/Square44x44Logo.targetsize-20_altform-lightunplated.png b/store_assets/Square44x44Logo.targetsize-20_altform-lightunplated.png new file mode 100644 index 0000000..be46c3d Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-20_altform-lightunplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-20_altform-unplated.png b/store_assets/Square44x44Logo.targetsize-20_altform-unplated.png new file mode 100644 index 0000000..be46c3d Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-20_altform-unplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-24.png b/store_assets/Square44x44Logo.targetsize-24.png new file mode 100644 index 0000000..5dcbeef Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-24.png differ diff --git a/store_assets/Square44x44Logo.targetsize-24_altform-lightunplated.png b/store_assets/Square44x44Logo.targetsize-24_altform-lightunplated.png new file mode 100644 index 0000000..5b60739 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-24_altform-lightunplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-24_altform-unplated.png b/store_assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000..5b60739 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-256.png b/store_assets/Square44x44Logo.targetsize-256.png new file mode 100644 index 0000000..c153097 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-256.png differ diff --git a/store_assets/Square44x44Logo.targetsize-256_altform-lightunplated.png b/store_assets/Square44x44Logo.targetsize-256_altform-lightunplated.png new file mode 100644 index 0000000..d9ce6ee Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-256_altform-lightunplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-256_altform-unplated.png b/store_assets/Square44x44Logo.targetsize-256_altform-unplated.png new file mode 100644 index 0000000..d9ce6ee Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-256_altform-unplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-30.png b/store_assets/Square44x44Logo.targetsize-30.png new file mode 100644 index 0000000..f643cf3 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-30.png differ diff --git a/store_assets/Square44x44Logo.targetsize-30_altform-lightunplated.png b/store_assets/Square44x44Logo.targetsize-30_altform-lightunplated.png new file mode 100644 index 0000000..c835f7a Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-30_altform-lightunplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-30_altform-unplated.png b/store_assets/Square44x44Logo.targetsize-30_altform-unplated.png new file mode 100644 index 0000000..c835f7a Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-30_altform-unplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-32.png b/store_assets/Square44x44Logo.targetsize-32.png new file mode 100644 index 0000000..23630ad Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-32.png differ diff --git a/store_assets/Square44x44Logo.targetsize-32_altform-lightunplated.png b/store_assets/Square44x44Logo.targetsize-32_altform-lightunplated.png new file mode 100644 index 0000000..fd3f150 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-32_altform-lightunplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-32_altform-unplated.png b/store_assets/Square44x44Logo.targetsize-32_altform-unplated.png new file mode 100644 index 0000000..fd3f150 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-32_altform-unplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-36.png b/store_assets/Square44x44Logo.targetsize-36.png new file mode 100644 index 0000000..9218c4e Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-36.png differ diff --git a/store_assets/Square44x44Logo.targetsize-36_altform-lightunplated.png b/store_assets/Square44x44Logo.targetsize-36_altform-lightunplated.png new file mode 100644 index 0000000..c5c23ba Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-36_altform-lightunplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-36_altform-unplated.png b/store_assets/Square44x44Logo.targetsize-36_altform-unplated.png new file mode 100644 index 0000000..c5c23ba Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-36_altform-unplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-40.png b/store_assets/Square44x44Logo.targetsize-40.png new file mode 100644 index 0000000..4270d11 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-40.png differ diff --git a/store_assets/Square44x44Logo.targetsize-40_altform-lightunplated.png b/store_assets/Square44x44Logo.targetsize-40_altform-lightunplated.png new file mode 100644 index 0000000..7f92ef0 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-40_altform-lightunplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-40_altform-unplated.png b/store_assets/Square44x44Logo.targetsize-40_altform-unplated.png new file mode 100644 index 0000000..7f92ef0 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-40_altform-unplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-48.png b/store_assets/Square44x44Logo.targetsize-48.png new file mode 100644 index 0000000..41f1bf1 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-48.png differ diff --git a/store_assets/Square44x44Logo.targetsize-48_altform-lightunplated.png b/store_assets/Square44x44Logo.targetsize-48_altform-lightunplated.png new file mode 100644 index 0000000..042d38d Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-48_altform-lightunplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-48_altform-unplated.png b/store_assets/Square44x44Logo.targetsize-48_altform-unplated.png new file mode 100644 index 0000000..042d38d Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-48_altform-unplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-60.png b/store_assets/Square44x44Logo.targetsize-60.png new file mode 100644 index 0000000..94a04f1 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-60.png differ diff --git a/store_assets/Square44x44Logo.targetsize-60_altform-lightunplated.png b/store_assets/Square44x44Logo.targetsize-60_altform-lightunplated.png new file mode 100644 index 0000000..3464dc2 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-60_altform-lightunplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-60_altform-unplated.png b/store_assets/Square44x44Logo.targetsize-60_altform-unplated.png new file mode 100644 index 0000000..3464dc2 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-60_altform-unplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-64.png b/store_assets/Square44x44Logo.targetsize-64.png new file mode 100644 index 0000000..7cf0e0f Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-64.png differ diff --git a/store_assets/Square44x44Logo.targetsize-64_altform-lightunplated.png b/store_assets/Square44x44Logo.targetsize-64_altform-lightunplated.png new file mode 100644 index 0000000..3e98f21 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-64_altform-lightunplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-64_altform-unplated.png b/store_assets/Square44x44Logo.targetsize-64_altform-unplated.png new file mode 100644 index 0000000..3e98f21 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-64_altform-unplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-72.png b/store_assets/Square44x44Logo.targetsize-72.png new file mode 100644 index 0000000..5a28fa0 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-72.png differ diff --git a/store_assets/Square44x44Logo.targetsize-72_altform-lightunplated.png b/store_assets/Square44x44Logo.targetsize-72_altform-lightunplated.png new file mode 100644 index 0000000..5b50525 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-72_altform-lightunplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-72_altform-unplated.png b/store_assets/Square44x44Logo.targetsize-72_altform-unplated.png new file mode 100644 index 0000000..5b50525 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-72_altform-unplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-80.png b/store_assets/Square44x44Logo.targetsize-80.png new file mode 100644 index 0000000..d6e88bd Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-80.png differ diff --git a/store_assets/Square44x44Logo.targetsize-80_altform-lightunplated.png b/store_assets/Square44x44Logo.targetsize-80_altform-lightunplated.png new file mode 100644 index 0000000..6a4fa21 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-80_altform-lightunplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-80_altform-unplated.png b/store_assets/Square44x44Logo.targetsize-80_altform-unplated.png new file mode 100644 index 0000000..6a4fa21 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-80_altform-unplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-96.png b/store_assets/Square44x44Logo.targetsize-96.png new file mode 100644 index 0000000..103fe35 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-96.png differ diff --git a/store_assets/Square44x44Logo.targetsize-96_altform-lightunplated.png b/store_assets/Square44x44Logo.targetsize-96_altform-lightunplated.png new file mode 100644 index 0000000..83f61b1 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-96_altform-lightunplated.png differ diff --git a/store_assets/Square44x44Logo.targetsize-96_altform-unplated.png b/store_assets/Square44x44Logo.targetsize-96_altform-unplated.png new file mode 100644 index 0000000..83f61b1 Binary files /dev/null and b/store_assets/Square44x44Logo.targetsize-96_altform-unplated.png differ diff --git a/store_package.json b/store_package.json index 260ac08..ce5d308 100644 --- a/store_package.json +++ b/store_package.json @@ -7,7 +7,7 @@ "publisher_id": "CN=52596601-BAB4-4F3F-B182-E8F3F273B202", "publisher_display": "Geiger", "publisher_name": "Lukas Geiger", - "version": "1.0.4.0", + "version": "1.0.5.0", "description": "Lokaler Markdown-Viewer und -Editor mit Lesemodus, Raw-Editor, PDF-Export, Mathe-Vorschau und DE/EN-Oberfläche.", "executable": "CleanMarkdown.exe", "capabilities": "runFullTrust", diff --git a/tests/test_assets_and_icons.py b/tests/test_assets_and_icons.py index 65afee9..0443984 100644 --- a/tests/test_assets_and_icons.py +++ b/tests/test_assets_and_icons.py @@ -159,6 +159,29 @@ def test_store_assets() -> None: ) +def test_filetype_icon_variants_match_store_tile() -> None: + """Regression test for T-20260927-699609650: .md-Dateien zeigten eine + Kachel auf einer Platte. Das Dateityp-/Taskleisten-Symbol kommt aus den + targetsize-/unplated-Varianten des Square44x44Logo (über resources.pri); + sie müssen vollständig vorliegen und das Kachel-Design zeigen, und + store_package.json muss die Dateitypen deklarieren, damit der MSIX-Builder + die FileTypeAssociation erzeugt.""" + store_dir = PROJECT_ROOT / "store_assets" + tile = Image.open(store_dir / "Square310x310Logo.png").convert("RGBA") + tile_rgb = Image.alpha_composite(Image.new("RGBA", tile.size, "white"), tile).convert("RGB") + for size in (16, 24, 32, 48, 256): + for altform in ("", "_altform-unplated", "_altform-lightunplated"): + path = store_dir / f"Square44x44Logo.targetsize-{size}{altform}.png" + assert path.is_file(), f"{path.name} fehlt" + img = Image.open(path).convert("RGBA") + assert img.size == (size, size), f"{path.name} hat Größe {img.size}" + rgb = Image.alpha_composite(Image.new("RGBA", img.size, "white"), img).convert("RGB") + diff = _perceptual_diff(rgb, tile_rgb) + assert diff <= CROSS_FILE_THRESHOLD, f"{path.name} weicht um {diff:.2f} von der Kachel ab" + store_pkg = json.loads((PROJECT_ROOT / "store_package.json").read_text(encoding="utf-8")) + assert ".md" in store_pkg["file_types"]["extensions"] + + def test_app_icon_loader_returns_valid_icon() -> None: from PySide6.QtWidgets import QApplication from main import load_app_icon diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 8ccfbf7..54c55b5 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -32,7 +32,7 @@ def test_pyproject_pep621_metadata(): content = pyproject_path.read_text(encoding="utf-8") assert 'name = "cleanmarkdown"' in content, "Package name mismatch" - assert 'version = "1.0.4"' in content, "Version mismatch" + assert 'version = "1.0.5"' in content, "Version mismatch" assert "Operating System :: OS Independent" in content, "Missing OS Independent classifier" assert "Programming Language :: Python :: 3.13" in content, "Missing Python 3.13 classifier" assert "Programming Language :: Python :: 3.12" in content, "Missing Python 3.12 classifier" @@ -134,9 +134,9 @@ def test_version_parity(): pyproject = (PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8") store_pkg = json.loads((PROJECT_ROOT / "store_package.json").read_text(encoding="utf-8")) - assert 'APP_VERSION = "1.0.4"' in main_py, "main.py version mismatch" - assert 'version = "1.0.4"' in pyproject, "pyproject.toml version mismatch" - assert store_pkg["version"] == "1.0.4.0", "store_package.json version mismatch" + assert 'APP_VERSION = "1.0.5"' in main_py, "main.py version mismatch" + assert 'version = "1.0.5"' in pyproject, "pyproject.toml version mismatch" + assert store_pkg["version"] == "1.0.5.0", "store_package.json version mismatch" def test_privacy_and_security_offline_invariants(): diff --git a/uninstall_local.ps1 b/uninstall_local.ps1 new file mode 100644 index 0000000..7e54fb4 --- /dev/null +++ b/uninstall_local.ps1 @@ -0,0 +1,245 @@ +# Rueckbau der lokalen Installation aus install_local.ps1, sobald das +# Store-Paket (MSIX) uebernommen hat. +# +# Hintergrund (T-20260927-699609650, Blocker D): install_local.ps1 legt eine +# eigene ProgID "CleanMarkdown.mdfile" mit DefaultIcon auf die lokale EXE in +# %LOCALAPPDATA%\Programs\CleanMarkdown an. Diese lokale Installation bleibt +# nach einer Store-Installation unveraendert bestehen und "gewinnt" weiterhin +# die .md-Dateizuordnung (bzw. haengt als olivgruene Kachel am Desktop) -- +# das MSIX ersetzt sie NICHT automatisch, egal wie oft es aktualisiert wird. +# Dieses Skript raeumt genau das auf, was install_local.ps1 angelegt hat -- +# und NUR, wenn ein Store-Paket mit einem tatsaechlichen .md-Handler bereits +# installiert ist (sonst bliebe der User ganz ohne .md-Handler zurueck). +# +# Empfohlene Reihenfolge (astra-Abnahme, T-20260927-699609650): +# 1. Passende Store-Version installieren/aktualisieren (mit .md-FileTypeAssociation +# im Manifest -- eine Version OHNE das, z.B. 1.0.3, wird von diesem Skript +# bewusst abgelehnt, siehe Schritt 1 unten). +# 2. .\uninstall_local.ps1 -WhatIf # Vorschau, keine Aenderung +# 3. .\uninstall_local.ps1 # gesicherter Rueckbau (fragt vorher nach) +# 4. Windows-Einstellungen > Apps > Standard-Apps > ".md" -> CleanMarkdown +# (Store) manuell als Standard waehlen -- der Rueckbau entfernt nur die +# ALTE Zuordnung, er setzt keine neue. +# 5. Ergebnis pruefen (.md-Icon im Explorer/Desktop). +# +# Nutzung: +# .\uninstall_local.ps1 # fuehrt den Rueckbau aus (fragt vorher nach) +# .\uninstall_local.ps1 -WhatIf # zeigt nur, was getan wuerde, KEINE Aenderung +# .\uninstall_local.ps1 -Force # ohne Rueckfrage (fuer Automatisierung) +# +# Sicherheitsnetz: Vor jeder Aenderung werden die betroffenen Registry-Zweige +# per `reg export` in einen eindeutigen Ordner je Lauf gesichert; schlaegt +# auch nur ein Export fehl, bricht das Skript VOR jeder Loeschung ab. + +param( + [switch]$WhatIf, + [switch]$Force +) + +$ErrorActionPreference = "Stop" + +$progId = "CleanMarkdown.mdfile" +$installDir = Join-Path $env:LOCALAPPDATA "Programs\CleanMarkdown" +$exeTarget = Join-Path $installDir "CleanMarkdown.exe" +$startMenuDir = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs" +$shortcutPath = Join-Path $startMenuDir "CleanMarkdown.lnk" +$classesRoot = "HKCU:\Software\Classes" +$fileExtsMd = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.md" +$userChoiceKey = "$fileExtsMd\UserChoice" + +# --- 1. Voraussetzung: Store-Paket MIT .md-Handler muss installiert sein --- +# Ein installiertes Paket allein genuegt nicht (astra-Fund D3): die auf +# diesem Host reale Version 1.0.3 ist installiert, ihr Manifest deklariert +# aber NULL FileTypeAssociation-Knoten fuer .md -- ein Rueckbau haette den +# User ganz ohne Handler zurueckgelassen. Deshalb zusaetzlich das Manifest +# des gefundenen Pakets pruefen, nicht nur seine blosse Existenz. +$storePkg = Get-AppxPackage -Name "Geiger.CleanMarkdown" -ErrorAction SilentlyContinue +if (-not $storePkg) { + Write-Host "ABBRUCH: Kein Store-Paket 'Geiger.CleanMarkdown' installiert." -ForegroundColor Red + Write-Host "Ohne Store-Paket wuerde dieser Rueckbau den User ganz ohne .md-Handler zuruecklassen." + Write-Host "Zuerst CleanMarkdown aus dem Microsoft Store installieren/aktualisieren, dann erneut ausfuehren." + exit 1 +} + +$manifestPath = Join-Path $storePkg.InstallLocation "AppxManifest.xml" +$hasMdHandler = $false +if (Test-Path $manifestPath) { + [xml]$manifestXml = Get-Content $manifestPath -Raw + $ns = New-Object System.Xml.XmlNamespaceManager($manifestXml.NameTable) + $ns.AddNamespace("uap", "http://schemas.microsoft.com/appx/manifest/uap/windows10") + $fileTypeNodes = $manifestXml.SelectNodes("//uap:FileTypeAssociation/uap:SupportedFileTypes/uap:FileType", $ns) + foreach ($node in $fileTypeNodes) { + if ($node.InnerText.Trim().ToLower() -eq ".md") { $hasMdHandler = $true; break } + } +} +if (-not $hasMdHandler) { + Write-Host "ABBRUCH: Store-Paket $($storePkg.PackageFullName) ist installiert, deklariert aber KEINE .md-FileTypeAssociation." -ForegroundColor Red + Write-Host "Ein Rueckbau jetzt wuerde den User ganz ohne .md-Handler zuruecklassen." + Write-Host "Zuerst auf eine Store-Version mit .md-Dateizuordnung aktualisieren, dann erneut ausfuehren." + exit 1 +} +Write-Host "Store-Paket mit .md-Handler gefunden: $($storePkg.PackageFullName)" -ForegroundColor Green + +# --- 2. Was wird entfernt? (reine Vorschau, KEINE Aenderung, KEIN Backup) --- +$actions = @() + +if (Test-Path "$classesRoot\$progId") { + $actions += "ProgID-Zweig entfernen: $classesRoot\$progId (inkl. DefaultIcon, shell\open\command)" +} +if (Test-Path "$classesRoot\Applications\CleanMarkdown.exe") { + $actions += "Applications-Zweig entfernen: $classesRoot\Applications\CleanMarkdown.exe" +} +$mdDefault = $null +if (Test-Path "$classesRoot\.md") { + $mdDefault = (Get-Item "$classesRoot\.md").GetValue("") +} +if ($mdDefault -eq $progId) { + $actions += "Default-Wert von $classesRoot\.md entfernen (zeigte auf $progId)" +} elseif ($mdDefault) { + $actions += "UEBERSPRUNGEN: $classesRoot\.md zeigt bereits auf '$mdDefault' (nicht mehr $progId) -- unveraendert gelassen" +} +if (Test-Path "$classesRoot\.md\OpenWithProgids") { + $prop = Get-ItemProperty -Path "$classesRoot\.md\OpenWithProgids" -Name $progId -ErrorAction SilentlyContinue + if ($prop) { $actions += "OpenWithProgids-Eintrag '$progId' unter $classesRoot\.md\OpenWithProgids entfernen" } +} +$userChoiceProgId = $null +if (Test-Path $userChoiceKey) { + $uc = Get-ItemProperty -Path $userChoiceKey -Name "ProgId" -ErrorAction SilentlyContinue + if ($uc) { $userChoiceProgId = $uc.ProgId } +} +if ($userChoiceProgId -eq $progId) { + # Nur den UserChoice-Unterschluessel selbst, NICHT den Elternzweig + # FileExts\.md (astra-Fund D1): der enthaelt auf realen Hosts zusaetzlich + # OpenWithList/OpenWithProgids/UserChoiceLatest, die install_local.ps1 + # nicht angelegt hat und die diesem Skript nicht gehoeren. + $actions += "UserChoice-Unterschluessel entfernen: $userChoiceKey (Explorer erinnerte sich an $progId als gewaehlten Handler; Geschwisterzweige unter FileExts\.md bleiben unangetastet)" +} elseif ($userChoiceProgId) { + $actions += "UEBERSPRUNGEN: UserChoice unter $fileExtsMd zeigt bereits auf '$userChoiceProgId' -- unveraendert gelassen" +} +if (Test-Path $shortcutPath) { + $actions += "Startmenü-Verknuepfung entfernen: $shortcutPath" +} +if (Test-Path $installDir) { + $actions += "Lokale Installation entfernen: $installDir" +} + +if ($actions.Count -eq 0) { + Write-Host "Nichts zu tun -- keine Spuren der lokalen Installation gefunden." -ForegroundColor Yellow + exit 0 +} + +Write-Host "`nFolgende Aenderungen werden vorgenommen:" +$actions | ForEach-Object { Write-Host " - $_" } + +if ($WhatIf) { + Write-Host "`n-WhatIf: keine Aenderung vorgenommen, kein Backup angelegt." -ForegroundColor Yellow + exit 0 +} + +if (-not $Force) { + $confirm = Read-Host "`nFortfahren? (j/N)" + if ($confirm -notin @("j", "J", "y", "Y")) { + Write-Host "Abgebrochen." -ForegroundColor Yellow + exit 0 + } +} + +# --- 3. Registry-Sicherung -- eindeutiger Ordner je Lauf, JEDER Export ------ +# wird geprueft; schlaegt auch nur einer fehl, bricht das Skript VOR +# jeder Loeschung ab (astra-Fund D2: ein gemockter Lauf mit 4 fehl- +# geschlagenen Exporten fuehrte vorher trotzdem alle Loeschungen aus). +$runId = Get-Date -Format "yyyyMMdd-HHmmss" +$backupDir = Join-Path $env:TEMP "CleanMarkdown-migration-backup\$runId" +New-Item -ItemType Directory -Force -Path $backupDir | Out-Null + +$keysToBackup = @( + "HKCU\Software\Classes\$progId", + "HKCU\Software\Classes\.md", + "HKCU\Software\Classes\Applications\CleanMarkdown.exe", + "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.md" +) +$backupFailed = $false +foreach ($key in $keysToBackup) { + $psPath = "Registry::$key" + if (-not (Test-Path $psPath)) { continue } + $exportName = ($key -replace '[\\]', '_') + ".reg" + $exportPath = Join-Path $backupDir $exportName + & reg export $key $exportPath /y 2>&1 | Out-Null + $exportOk = ($LASTEXITCODE -eq 0) -and (Test-Path $exportPath) -and ((Get-Item $exportPath).Length -gt 0) + if (-not $exportOk) { + Write-Host " FEHLER: Backup fehlgeschlagen fuer $key (Exit $LASTEXITCODE, Datei $exportPath)" -ForegroundColor Red + $backupFailed = $true + } +} +if ($backupFailed) { + Write-Host "`nABBRUCH: Mindestens eine Registry-Sicherung ist fehlgeschlagen -- KEINE Aenderung vorgenommen." -ForegroundColor Red + Write-Host "Teilweise angelegte Sicherungen liegen (zur Fehlersuche) in: $backupDir" + exit 1 +} +Write-Host "Registry-Sicherung vollstaendig: $backupDir" -ForegroundColor Cyan + +# --- 4. Rueckbau durchfuehren ------------------------------------------------ +$incomplete = $false + +if (Test-Path "$classesRoot\$progId") { + Remove-Item -Path "$classesRoot\$progId" -Recurse -Force +} +if (Test-Path "$classesRoot\Applications\CleanMarkdown.exe") { + Remove-Item -Path "$classesRoot\Applications\CleanMarkdown.exe" -Recurse -Force +} +if ($mdDefault -eq $progId) { + # Remove-ItemProperty -Name "(Default)"/"(default)" does NOT work against + # the registry provider (fails silently under -ErrorAction + # SilentlyContinue, the value is left unchanged) -- verified in both + # PowerShell 7.6.6 and Windows PowerShell 5.1 (D4, Fable-Abnahme + # T-20260927-699609650). Left as-is, .md would keep pointing at the + # ProgID just deleted above -- a dangling reference, worse than the + # previous Set-Item -Value "" (which at least emptied it). Delete the + # default value directly via the .NET registry API instead. + $mdKey = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey("Software\Classes\.md", $true) + if ($mdKey) { + try { $mdKey.DeleteValue("", $false) } finally { $mdKey.Close() } + } +} +if (Test-Path "$classesRoot\.md\OpenWithProgids") { + Remove-ItemProperty -Path "$classesRoot\.md\OpenWithProgids" -Name $progId -ErrorAction SilentlyContinue +} +if ($userChoiceProgId -eq $progId) { + try { + # Nur den Unterschluessel, siehe Begruendung in der Vorschau oben. + Remove-Item -Path $userChoiceKey -Recurse -Force + } catch { + $incomplete = $true + Write-Host " HINWEIS: UserChoice-Unterschluessel ($userChoiceKey) konnte nicht geloescht werden (Windows schuetzt diesen Schluessel oft per ACL gegen programmatisches Loeschen): $($_.Exception.Message)" -ForegroundColor Yellow + Write-Host " Der User muss die Zuordnung manuell neu setzen: Einstellungen > Apps > Standard-Apps > '.md' waehlen." + } +} +if (Test-Path $shortcutPath) { + Remove-Item -Path $shortcutPath -Force +} +if (Test-Path $installDir) { + Remove-Item -Path $installDir -Recurse -Force +} + +# --- 5. Shell-Icon-Cache aktualisieren -------------------------------------- +$signature = @' +using System; +using System.Runtime.InteropServices; +public static class ShellNotify { + [DllImport("shell32.dll")] + public static extern void SHChangeNotify(uint wEventId, uint uFlags, IntPtr dwItem1, IntPtr dwItem2); +} +'@ +Add-Type -TypeDefinition $signature | Out-Null +[ShellNotify]::SHChangeNotify(0x08000000, 0x0000, [IntPtr]::Zero, [IntPtr]::Zero) + +if ($incomplete) { + Write-Host "`nRueckbau ABGESCHLOSSEN MIT EINSCHRAENKUNG -- siehe Hinweis oben (manueller Folgeschritt noetig)." -ForegroundColor Yellow +} else { + Write-Host "`nRueckbau abgeschlossen." -ForegroundColor Green +} +Write-Host "Registry-Sicherung liegt in: $backupDir" +Write-Host "Als NAECHSTES: Einstellungen > Apps > Standard-Apps > '.md' -> CleanMarkdown (Store) waehlen --" +Write-Host "dieses Skript entfernt nur die alte Zuordnung, es setzt keine neue." +Write-Host "Falls .md danach kein korrektes Icon zeigt: einmal abmelden/anmelden oder Explorer neu starten" +Write-Host "(taskkill /f /im explorer.exe && start explorer.exe), damit der Icon-Cache neu aufbaut."