73 Commits
Author SHA1 Message Date
M_Viper f75f3fb9c6 Upload via GUI (27 Dateien) 2026-10-22 20:14:17 +00:00
M_Viper 6e1cee9a59 Delete 16 files via Git Manager GUI 2026-10-22 09:14:12 +00:00
M_Viper 2e26aaa41c Delete 100 files via Git Manager GUI 2026-10-22 09:14:11 +00:00
M_Viper eb13bd071b Delete 100 files via Git Manager GUI 2026-10-22 09:14:10 +00:00
M_Viper 156b8e3041 Delete 100 files via Git Manager GUI 2026-10-22 09:14:08 +00:00
M_Viper 9d3472dff0 Delete 100 files via Git Manager GUI 2026-10-22 09:14:06 +00:00
M_Viper 0029668033 Delete 100 files via Git Manager GUI 2026-10-22 09:14:05 +00:00
M_Viper a809efd91a Delete 100 files via Git Manager GUI 2026-10-22 09:14:03 +00:00
M_Viper a902a0c5d7 Delete 100 files via Git Manager GUI 2026-10-22 09:14:02 +00:00
M_Viper 7fa0db02b5 Delete 100 files via Git Manager GUI 2026-10-22 09:14:00 +00:00
M_Viper 673affe1d1 Delete 100 files via Git Manager GUI 2026-10-22 09:13:57 +00:00
M_Viper 65dd67cef5 Delete 100 files via Git Manager GUI 2026-10-22 09:13:53 +00:00
M_Viper ef68a305ce Delete 100 files via Git Manager GUI 2026-10-22 09:13:49 +00:00
M_Viper e154cc507e Delete 100 files via Git Manager GUI 2026-10-22 09:13:46 +00:00
M_Viper 56b5cb17d0 Delete 100 files via Git Manager GUI 2026-10-22 09:13:44 +00:00
M_Viper e178605833 Delete 100 files via Git Manager GUI 2026-10-22 09:13:42 +00:00
M_Viper f4c16b173f Upload via GUI (85 Dateien) 2026-10-22 09:13:21 +00:00
M_Viper 2ad8777311 Upload via GUI (85 Dateien) 2026-10-22 09:13:16 +00:00
M_Viper ce5a655758 Delete 13 files via Git Manager GUI 2026-10-22 07:39:41 +00:00
M_Viper 7660b5de1a Delete data/credentials.json via Git Manager GUI 2026-10-22 07:39:34 +00:00
M_Viper 50705d1197 Delete .claude/settings.local.json via Git Manager GUI 2026-10-22 07:39:18 +00:00
M_Viper 1f635d33cc Upload via GUI (60 Dateien) 2026-10-22 07:22:51 +00:00
M_Viper 38a75bc720 Upload via GUI (60 Dateien) 2026-10-22 07:22:49 +00:00
M_Viper ead123c62b Upload via GUI (12 Dateien) 2026-10-22 07:16:16 +00:00
M_Viper 9b3aed6f0a Upload via GUI (28 Dateien) 2026-10-22 07:15:59 +00:00
M_Viper 99d9629e25 Upload via GUI (10 Dateien) 2026-10-22 07:15:41 +00:00
M_Viper 85a222a9fb Upload via GUI (7543 Dateien) 2026-10-22 07:03:23 +00:00
M_Viper dedb09b9c6 Upload via GUI (7543 Dateien) 2026-10-22 07:03:19 +00:00
M_Viper c45ad3ba32 Upload via GUI (7543 Dateien) 2026-10-22 07:03:16 +00:00
M_Viper 937c72ce4a Upload via GUI (7543 Dateien) 2026-10-22 07:03:12 +00:00
M_Viper 61d6d11a2f Upload via GUI (7543 Dateien) 2026-10-22 07:03:09 +00:00
M_Viper ef911afb06 Upload via GUI (7543 Dateien) 2026-10-22 07:03:04 +00:00
M_Viper 4f9494ae7b Upload via GUI (7543 Dateien) 2026-10-22 07:03:00 +00:00
M_Viper 37820af6dd Upload via GUI (7543 Dateien) 2026-10-22 07:02:57 +00:00
M_Viper 9c6a69cc53 Upload via GUI (7543 Dateien) 2026-10-22 07:02:52 +00:00
M_Viper eb549d3975 Upload via GUI (7543 Dateien) 2026-10-22 07:02:42 +00:00
M_Viper b2314f6fe6 Upload via GUI (7543 Dateien) 2026-10-22 07:02:28 +00:00
M_Viper 42fb8dfe9b Upload via GUI (7543 Dateien) 2026-10-22 07:02:04 +00:00
M_Viper 54baf9b874 Upload via GUI (7543 Dateien) 2026-10-22 07:01:41 +00:00
M_Viper 8d29040b57 Upload via GUI (7543 Dateien) 2026-10-22 07:01:11 +00:00
M_Viper 8c7b2ddf47 Upload via GUI (7543 Dateien) 2026-10-22 07:00:36 +00:00
M_Viper 03f4fa6fbc Upload via GUI (7543 Dateien) 2026-10-22 06:59:59 +00:00
M_Viper a57786fe54 Upload via GUI (7543 Dateien) 2026-10-22 06:59:38 +00:00
M_Viper 6f73a87314 Upload via GUI (7543 Dateien) 2026-10-22 06:59:13 +00:00
M_Viper 7ef5522258 Upload via GUI (7543 Dateien) 2026-10-22 06:58:47 +00:00
M_Viper 860c714301 Upload via GUI (7543 Dateien) 2026-10-22 06:58:16 +00:00
M_Viper 68133f4d0f Upload via GUI (7543 Dateien) 2026-10-22 06:57:52 +00:00
M_Viper 75cc88a011 Upload via GUI (7543 Dateien) 2026-10-22 06:57:20 +00:00
M_Viper bac355eabb Upload via GUI (7543 Dateien) 2026-10-22 06:56:47 +00:00
M_Viper d7658c873f Upload via GUI (7543 Dateien) 2026-10-22 06:56:30 +00:00
M_Viper 8830aac76c Upload via GUI (7543 Dateien) 2026-10-22 06:56:23 +00:00
M_Viper 336d9e600b Upload via GUI (7543 Dateien) 2026-10-22 06:56:21 +00:00
M_Viper d6518698b1 Upload via GUI (7543 Dateien) 2026-10-22 06:56:19 +00:00
M_Viper e4585e50ee Upload via GUI (7543 Dateien) 2026-10-22 06:56:17 +00:00
M_Viper 12a2f5faa9 Upload via GUI (7543 Dateien) 2026-10-22 06:56:16 +00:00
M_Viper 8aede65bd7 Upload via GUI (7543 Dateien) 2026-10-22 06:56:15 +00:00
M_Viper ae68900363 Upload via GUI (7543 Dateien) 2026-10-22 06:55:39 +00:00
M_Viper 81c541a0b5 Upload via GUI (7543 Dateien) 2026-10-22 06:55:33 +00:00
M_Viper 089257d740 Upload via GUI (7543 Dateien) 2026-10-22 06:55:01 +00:00
M_Viper 2ecc57c761 Upload via GUI (7543 Dateien) 2026-10-22 06:54:55 +00:00
M_Viper 4654eed12c Upload via GUI (7543 Dateien) 2026-10-22 06:54:26 +00:00
M_Viper 434a4e1c7b Upload via GUI (7543 Dateien) 2026-10-22 06:54:18 +00:00
M_Viper a0546c562d Upload via GUI (7543 Dateien) 2026-10-22 06:54:05 +00:00
M_Viper 264f1b7e45 Upload via GUI (7543 Dateien) 2026-10-22 06:54:02 +00:00
M_Viper 3b31c5893c Upload via GUI (7543 Dateien) 2026-10-22 06:53:49 +00:00
M_Viper 047196c4d0 Upload via GUI (7543 Dateien) 2026-10-22 06:53:45 +00:00
M_Viper 5571f225d5 Upload via GUI (27 Dateien) 2026-08-13 07:21:45 +00:00
Git Manager GUI 565835418e Upload via GUI (31 Dateien) 2026-07-16 08:01:04 +02:00
Git Manager GUI 3ed561fe78 Upload via Git Manager GUI 2026-05-24 22:33:27 +02:00
Git Manager GUI fe6c31793e Upload via Git Manager GUI 2026-05-24 22:33:22 +02:00
Git Manager GUI 4bb948d665 Upload via Git Manager GUI 2026-05-24 22:33:20 +02:00
Git Manager GUI 5c792eb1a8 Upload folder via GUI - src 2026-05-24 22:33:18 +02:00
Git Manager GUI 309babf849 Upload folder via GUI - renderer 2026-05-24 22:33:16 +02:00
139 changed files with 20825 additions and 8581 deletions
+5
View File
@@ -0,0 +1,5 @@
* text=auto eol=lf
*.png binary
*.ico binary
*.exe binary
*.blockmap binary
+58
View File
@@ -0,0 +1,58 @@
# ===== Zugangsdaten / Secrets (NIE hochladen!) =====
/data/
credentials.json
*.env
.env*
*.pem
*.key
*.pfx
*.p12
# ===== Abhängigkeiten =====
node_modules/
.npm/
.cache/
# ===== Build-Ausgaben =====
dist/
out/
release/
*.exe
*.blockmap
*.sha256
*.sha256sum
# ===== .NET Installer (Visual Studio / dotnet) =====
installer/bin/
installer/obj/
**/bin/
**/obj/
.vs/
*.user
*.suo
# ===== Große gebündelte Binaries (MinGit, ca. 93 MB) =====
vendor/mingit/
# ===== Laufzeitdaten / Backups / Repos =====
/repos/
/backup/
*.zip
*.rar
*.7z
# ===== Logs =====
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# ===== Editor / OS =====
.vscode/
.idea/
.DS_Store
Thumbs.db
desktop.ini
# ===== Claude Code (lokale Einstellungen) =====
.claude/settings.local.json
+50 -1
View File
@@ -282,9 +282,58 @@ npm run build
# Output: dist/git-manager-gui-Setup-2.x.x.exe
```
### Abbrechbare Vorgänge
Uploads, Ordner-Downloads und Batch-Clones laufen über **ein** gemeinsames Abbruch-System:
- `main.js`: `beginCancellableUpload()` legt einen `AbortController` an, `endCancellableUpload()` räumt auf (immer im `finally`). Der IPC-Kanal `cancel-upload` bricht alle laufenden Vorgänge ab.
- Das `signal` wird durchgereicht an `simple-git` (`abort`), `execGit`/`runGitAsync` (`signal`), `runLimited` und `uploadItemsViaGiteaApi` (axios `signal`).
- Ein Abbruch ist **kein Fehler**: Er läuft als `UPLOAD_CANCELLED` durch die Kette (`createUploadCancelledError`, `isUploadCancelledError`, `throwIfUploadCancelled` in `src/git/apiHandler.js`), löst keinen API-Ausweichweg aus und kommt als `{ ok: false, cancelled: true }` im Renderer an.
- Neuer abbrechbarer Handler: `beginCancellableUpload()` am Anfang, `signal` weiterreichen, im `catch` `isUploadCancelledError(e)` abfangen, im `finally` `endCancellableUpload()`. Im Renderer `showProgress(..., { cancellable: true })`.
- Unter Windows beendet `spawn(signal)` nur `git.exe`; `execGit` beendet deshalb den ganzen Prozessbaum (`taskkill /T /F`) und kehrt sofort zurück.
### Pause, Sperre, Benachrichtigung, Release-Assistent
- **Pause:** `src/features/pauseGate.js` (`createPauseGate`). In `main.js` gibt es genau ein `transferGate`. Kontrollpunkte rufen `await transferGate.wait(signal)` auf (in `runLimited`, vor dem Staging der Git-Wege, zwischen den Paketen von `uploadItemsViaGiteaApi` über `waitIfPaused`, vor jedem Batch-Clone). Ein Abbruch löst Wartende sofort aus. Neue langlaufende Schritte brauchen einen eigenen Kontrollpunkt.
- **Zugangsdaten-Sperre:** `src/features/uploadBlocklist.js` nutzt die Dateinamen-Regeln aus `secretScan.js`. `removeBlockedInPlace()` und `blockedSecretReason()` in `main.js` sind in allen Upload-Handlern eingebaut, die Ergebnisse tragen `blockedFiles`. Neuer Upload-Weg: dort ebenfalls aufrufen. Einstellung `uploadBlockSecrets`.
- **Benachrichtigung:** `src/features/transferNotice.js` baut den Text aus dem Verlaufseintrag, `maybeNotifyTransfer()` in `main.js` zeigt ihn nur, wenn das Hauptfenster nicht vorn ist (Einstellung `notifyTransfers`).
- **Release-Assistent:** `src/features/releaseAssist.js` (Version aus Dateiname, Versionsvergleich, Tag-Prüfung, Release-Text mit Prüfsummen). IPC `inspect-release-file` und `prepare-release`, Oberfläche in `showCreateReleaseModal()`. Das Prüfsummen-Format in `buildReleaseBody` muss zu `extractChecksumFromReleaseBody` in `updater.js` passen.
### Startanimation
Nach dem Vorbild des AeroMC-Launchers (`AeroMC-Launcher/src/startanimation.js`):
1. `createWindow()` berechnet Position und Größe (`centeredBounds`) und legt über `begin()` zuerst ein randloses, durchsichtiges Fenster mit `renderer/splash.html` an: das Logo dreht sich (CSS `rotateY`), darunter der Schriftzug.
2. Das Hauptfenster entsteht danach mit denselben Maßen, verborgen, und lädt.
3. `play()` wartet auf `did-finish-load` (höchstens 12 s) und mindestens 1,6 s Drehzeit. Dann zeigt es das Hauptfenster unsichtbar (Deckkraft 0, Mausereignisse aus), wartet auf die gezeichnete Oberfläche (Repo-Liste oder höchstens 1,5 s), fotografiert es mit `capturePage()` und lässt `aufbauen()` in `splash.js` das Bild von der Mitte aus aufziehen.
4. Am Ende übernimmt das echte Fenster an derselben Stelle. Fehler und Zeitüberschreitungen zeigen das Hauptfenster immer trotzdem.
**Stolperstein:** `setIgnoreMouseEvents(true)` am Startfenster darf erst nach dem Zeigen und leicht verzögert aufgerufen werden (`IGNORE_MOUSE_DELAY_MS`). Davor aufgerufen wird das durchsichtige Fenster unter Windows weiß. `scripts/ui-smoke-splash.js` prüft das über eine Bildschirmaufnahme (Farbe an einer leeren Stelle muss der des Desktops entsprechen) und darf deshalb die Hardware-Beschleunigung nicht abschalten.
Beim Start mit `--hidden` und bei abgeschalteter Einstellung `showSplash` gibt es die Animation nicht. `scripts/ui-smoke-splash.js` prüft sie mit einem Ersatz-Hauptfenster. Im Entwicklungsmodus (`electron .`) startet die App wegen der vielen `node_modules`-Dateien deutlich langsamer als das gepackte Programm; die Animation selbst wird dadurch nicht langsamer.
### Oberflächen-Testlauf
`npm run test:ui` (`scripts/ui-smoke.js`) startet die App mit leerem eigenen `APPDATA`, steuert sie über den Debug-Port und prüft Toolbar-Layout bei drei Breiten, Fortschrittsbalken, Pause, Verlauf, Sperre, Release-Prüfung und Vorlagen. `scripts/ui-smoke-tray.js` prüft das Tray-Menüfenster in echtem Electron. Der Lauf berührt weder echte Zugangsdaten noch einen Server und dauert gut eine Minute.
### Tray-Menü
Das Tray-Symbol öffnet kein natives Menü, sondern ein eigenes kleines Fenster, denn das native Electron-Menü zeichnet unter Windows keine Hover-Markierung:
- `src/features/trayMenu.js`: Inhalt (`buildTrayTemplate`, `serializeTemplate`, `runAction`, `trayTooltip`), ohne Electron und damit testbar. Neuer Eintrag: dort mit `id` ergänzen, die Aktion in `trayTemplate()` in `main.js` verdrahten.
- `src/features/trayPopup.js`: das Fenster (frameless, transparent, schließt bei Fokusverlust und Esc). Es wird 2 Sekunden nach dem Start vorgeladen, weil der erste Renderer-Start mehrere Sekunden dauert.
- `renderer/tray-menu.html`, `renderer/tray-menu.js`, `tray-menu-preload.js`: Darstellung (hell/dunkel nach Windows-Design, Pfeiltasten und Enter) und die vier IPC-Kanäle.
- Das Fenster ist von der Zoom-Einstellung ausgenommen und wird beim Beenden zerstört, sonst hielte das versteckte Fenster die App am Leben.
### Upload-Verlauf und Wiederholen
- `src/features/uploadHistory.js` (`buildEntry`, `filterEntries`, `createUploadHistory`): speichert die letzten 100 Uploads in `upload-history.json` im userData-Ordner. `main.js` umhüllt dafür `ipcMain.handle` für die Kanäle in `UPLOAD_HISTORY_CHANNELS`, neue Upload-Kanäle dort eintragen.
- Teilfehler: `buildPartialUploadError` hängt die fehlgeschlagenen Einträge an den Fehler (`failedItems`). Die Handler geben sie als `retryItems` zurück, der Renderer sendet sie über den Parameter `items` erneut (`retryFailedUploads`).
- `.gitignore`-Vorlagen: `src/features/gitignoreTemplates.js`.
### Version erhöhen
1. Version in `package.json` updaten: `"version": "2.1.4"`
1. Version in `package.json` updaten: `"version": "2.2.1"`
2. Changelog aktualisieren (optional)
3. Build ausführen
4. GitHub Release erstellen mit der .exe
+70 -1
View File
@@ -1250,6 +1250,73 @@ tr:last-child td { border-bottom: none; }
</div>
</div>
<div class="chapter">
<h2 class="chapter-heading"><span class="num">12a</span> Uploads: Abbrechen, Wiederholen &amp; Verlauf</h2>
<div class="feature-grid">
<div class="feature-cell">
<div class="icon">⏹️</div>
<h4>Abbrechen</h4>
<p>Während eines Uploads, Ordner-Downloads oder Batch-Clones erscheint im Fortschrittsbalken oben der Knopf <kbd>Abbrechen (Esc)</kbd>. Du kannst auch einfach <kbd>Esc</kbd> drücken, solange kein Dialog offen ist.</p>
</div>
<div class="feature-cell">
<div class="icon">⏱️</div>
<h4>Fortschritt</h4>
<p>Der Balken zeigt Dateien, übertragene MB und eine geschätzte Restzeit, zum Beispiel <em>Upload: 48/120 Dateien · 19,2 MB / 48,0 MB · noch ca. 5 Sek.</em> Die Restzeit ist eine Hochrechnung und erscheint erst nach einigen Sekunden.</p>
</div>
<div class="feature-cell">
<div class="icon">🔁</div>
<h4>Nur Fehlgeschlagene wiederholen</h4>
<p>Schlagen bei einem Upload einzelne Dateien fehl, fragt die App nach und sendet nur diese erneut (bis zu dreimal). Bereits hochgeladene Dateien bleiben unberührt.</p>
</div>
<div class="feature-cell">
<div class="icon">⏸️</div>
<h4>Pausieren und Fortsetzen</h4>
<p>Neben <kbd>Abbrechen</kbd> im Fortschrittsbalken gibt es <kbd>Pausieren</kbd>. Die Übertragung hält zwischen den Schritten an (ein laufender Clone, Push oder ein Paket wird noch fertig) und läuft mit <kbd>Fortsetzen</kbd> weiter. Das geht auch über das Tray-Menü.</p>
</div>
<div class="feature-cell">
<div class="icon">🕘</div>
<h4>Upload-Verlauf</h4>
<p>Über <kbd>🕘 Verlauf</kbd> in der Toolbar siehst du die letzten 100 Uploads mit Datum, Repo, Dateizahl, Dauer und Ergebnis. Filtere nach Erfolgreich, Abgebrochen oder Fehler und suche nach einem Repo-Namen. Ein Klick auf einen Eintrag öffnet das Repo, <kbd>CSV exportieren</kbd> speichert die gefilterte Liste für Excel.</p>
</div>
<div class="feature-cell">
<div class="icon">🛡️</div>
<h4>Zugangsdaten-Sperre</h4>
<p>Dateien wie <code>credentials.json</code>, <code>.env</code>, private Schlüssel oder Keystores werden auf allen Upload-Wegen ausgelassen, auch wenn du sie in der Vorschau nicht abgewählt hast. Die App meldet, welche Dateien fehlen. Vorlagen wie <code>.env.example</code> bleiben erlaubt. Unter Einstellungen → Uploads lässt sich die Sperre abschalten, bei öffentlichen Repos solltest du sie aktiv lassen.</p>
</div>
<div class="feature-cell">
<div class="icon">🔔</div>
<h4>Windows-Meldung</h4>
<p>Endet ein Upload, während die App im Tray versteckt, minimiert oder nicht im Vordergrund ist, meldet Windows das Ergebnis. Ein Klick auf die Meldung öffnet die App. Abgebrochene Uploads melden nichts. Unter Einstellungen → Uploads lässt es sich abschalten.</p>
</div>
</div>
<div class="warn-box">
<strong>Grenzen des Abbrechens:</strong> Ein Push, der schon auf dem Server angekommen ist, lässt sich nicht zurücknehmen. Beim Upload über die Gitea-API bleiben Pakete, die schon als Commit verbucht sind, im Repo. Eine Migration kann nicht abgebrochen werden, weil der Server sie selbst ausführt. Das Repo-Backup hat seinen eigenen Abbrechen-Knopf.
</div>
<div class="card">
<div class="card-title">📦 Release für den Updater veröffentlichen</div>
<p>Der Updater liest die Releases des Repos <code>M_Viper/Git-Manager-Gui</code> auf Gitea. So veröffentlichst du eine neue Version:</p>
<ol class="steps">
<li>Baue den Installer mit <code>npm run build</code>. Dabei entstehen <code>Git-Manager-GUI-Setup-X.Y.Z.exe</code> und die Prüfsummen-Datei <code>.sha256</code> in <code>dist/</code>.</li>
<li>Öffne in der Repo-Ansicht <strong>🚀 Releases</strong> und lege ein neues Release an. Wähle bei <strong>Release Asset</strong> den Installer.</li>
<li>Die App berechnet die SHA-256-Prüfsumme, gleicht sie mit der <code>.sha256</code>-Datei ab und füllt Tag und Name aus dem Dateinamen vor. Stimmen die Prüfsummen nicht überein, wird nichts veröffentlicht.</li>
<li>Beim Erstellen prüft die App, dass der Tag eine Version wie <code>v2.2.1</code> ist, es ihn noch nicht gibt und er neuer ist als das letzte Release, sonst würde der Updater ihn nicht anbieten.</li>
<li>Die Prüfsumme wird im Format, das der Updater liest, in den Release-Text geschrieben, und Installer und <code>.sha256</code> werden hochgeladen.</li>
</ol>
</div>
<div class="card">
<div class="card-title">📄 .gitignore im Repo anlegen</div>
<ol class="steps">
<li>Öffne ein Repository und klicke in der Repo-Leiste auf <strong>📄 .gitignore</strong>.</li>
<li>Wähle die Vorlagen: Node.js, .NET, Electron, Python, Java, PHP, Unity. Der allgemeine Teil (Betriebssystem, Editor, <code>.env</code>, Logs) ist immer dabei.</li>
<li>Mit <strong>Anlegen</strong> wird die Datei ins Repo geschrieben. Existiert schon eine, fragt die App vor dem Ersetzen nach.</li>
</ol>
</div>
</div>
<!-- ──────────────────────────────────────────── -->
<div class="part-break">
<div class="part-label">TEIL F</div>
@@ -1274,8 +1341,10 @@ tr:last-child td { border-bottom: none; }
<tr><th>Einstellung</th><th>Standard</th><th>Beschreibung</th></tr>
<tr><td>⭐ Favoriten anzeigen</td><td>An</td><td>Zeigt/versteckt die Favoriten-Sidebar und Stern-Buttons</td></tr>
<tr><td>🕐 Zuletzt geöffnet</td><td>An</td><td>Verlaufs-Sidebar ein-/ausblenden</td></tr>
<tr><td>🔍 Symbolgröße</td><td>Klein</td><td>Größe der Symbole in der Repo-Übersicht und im Datei-Explorer: Klein, Mittel oder Groß. Klein wirkt ruhiger und zeigt mehr auf einmal. Wird sofort angewendet (Einstellungen → Darstellung)</td></tr>
<tr><td>⊞ Kompakt-Modus</td><td>Aus</td><td>Verdichtete Darstellung für kleinere Bildschirme</td></tr>
<tr><td>🎨 Farbige Datei-Icons</td><td>An</td><td>Unterschiedliche Farben je Dateityp im Explorer</td></tr>
<tr><td>✨ Startanimation</td><td>An</td><td>Beim Öffnen dreht sich das Logo, danach baut sich das Fenster von der Mitte aus auf. Beim Start mit Windows im Tray gibt es sie nie</td></tr>
<tr><td>🚀 Mit Windows starten</td><td>Aus</td><td>App miniert im System-Tray beim Windows-Login starten</td></tr>
</table>
@@ -1303,7 +1372,7 @@ tr:last-child td { border-bottom: none; }
<tr><td><kbd>Ctrl</kbd>+<kbd>K</kbd></td><td>Fuzzy-Suche im Repository-Grid fokussieren</td></tr>
<tr><td><kbd>Ctrl</kbd>+<kbd>S</kbd></td><td>Aktive Datei im Editor speichern</td></tr>
<tr><td><kbd>Ctrl</kbd>+<kbd>F</kbd></td><td>Suchleiste im Editor öffnen</td></tr>
<tr><td><kbd>Escape</kbd></td><td>Suchleiste im Editor schließen</td></tr>
<tr><td><kbd>Escape</kbd></td><td>Suchleiste im Editor schließen · laufenden Upload/Download abbrechen (wenn kein Dialog offen ist)</td></tr>
<tr><td><kbd>Rechtsklick</kbd></td><td>Kontextmenü auf Repository-Karte oder Sidebar-Eintrag</td></tr>
<tr><td><kbd>Drag</kbd> raus</td><td>Datei/Ordner/Repo herunterladen</td></tr>
<tr><td><kbd>Drag</kbd> rein</td><td>Datei/Ordner in aktuelles Verzeichnis hochladen</td></tr>
+53 -2
View File
@@ -1,12 +1,39 @@
# 🛠 Git Manager Explorer Pro
[![GitHub Release](https://img.shields.io/badge/version-2.1.3-blue.svg)](https://github.com/M-Viper/git-manager-gui/releases)
[![GitHub Release](https://img.shields.io/badge/version-2.2.1-blue.svg)](https://github.com/M-Viper/git-manager-gui/releases)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![Windows Support](https://img.shields.io/badge/platform-Windows-0078D4.svg)](https://github.com/M-Viper/git-manager-gui)
**Eine moderne Desktop-Anwendung für vollständige Git-Repository-Verwaltung mit Gitea & GitHub.**
Verwalte deine Repositories intuitiv ohne Kommandozeile — mit Favoriten, Verlauf, Editor und Batch-Aktionen.
<p align="center">
<img src="docs/screenshots/start.gif" alt="Programmstart: das Logo dreht sich, danach baut sich das Fenster von der Mitte aus auf" width="720">
<br>
<sub>✨ Der Start: Das Logo dreht sich, danach baut sich das Fenster von der Mitte aus auf</sub>
</p>
<p align="center">
<a href="docs/demo.mp4"><img src="docs/screenshots/thumbs/video.jpg" alt="Video: Rundgang durch die App" width="360"></a>
<br>
<sub>🎬 <a href="docs/demo.mp4"><b>Rundgang als Video ansehen</b></a> (1½ Minuten, mit Mauszeiger und Erklärungen, ohne Ton)</sub>
</p>
---
## 🖼️ Bilder
Alle Bilder zeigen erfundene Beispieldaten. Ein Klick auf ein Vorschaubild öffnet es in voller Größe.
| | | |
|:---:|:---:|:---:|
| [![Übersicht](docs/screenshots/thumbs/uebersicht.png)](docs/screenshots/uebersicht.png)<br>📂 **Übersicht**<br><sub>Repos nach Besitzer, Favoriten, Heatmap</sub> | [![Repo-Ansicht](docs/screenshots/thumbs/repo.png)](docs/screenshots/repo.png)<br>📁 **Repo-Ansicht**<br><sub>Dateien, Leiste für Commits, Releases, PRs</sub> | [![Editor](docs/screenshots/thumbs/editor.png)](docs/screenshots/editor.png)<br>✏️ **Editor**<br><sub>Dateien direkt im Repo bearbeiten</sub> |
| [![Commit-Verlauf](docs/screenshots/thumbs/commits.png)](docs/screenshots/commits.png)<br>📊 **Commit-Verlauf**<br><sub>Autor, Zeit und Änderungen</sub> | [![Releases](docs/screenshots/thumbs/releases.png)](docs/screenshots/releases.png)<br>📦 **Releases**<br><sub>Mit Dateien und Prüfsummen</sub> | [![Release-Assistent](docs/screenshots/thumbs/release-assistent.png)](docs/screenshots/release-assistent.png)<br>🚀 **Release-Assistent**<br><sub>Installer und Prüfsumme prüfen</sub> |
| [![Upload-Vorschau](docs/screenshots/thumbs/upload-vorschau.png)](docs/screenshots/upload-vorschau.png)<br>🛡️ **Upload-Vorschau**<br><sub>Zugangsdaten werden gesperrt</sub> | [![Fortschritt](docs/screenshots/thumbs/fortschritt.png)](docs/screenshots/fortschritt.png)<br>⏸️ **Fortschritt**<br><sub>Pausieren, Abbrechen, MB, Restzeit</sub> | [![Upload-Verlauf](docs/screenshots/thumbs/verlauf.png)](docs/screenshots/verlauf.png)<br>🕘 **Upload-Verlauf**<br><sub>Filter, CSV-Export</sub> |
| [![Einstellungen](docs/screenshots/thumbs/einstellungen.png)](docs/screenshots/einstellungen.png)<br>⚙️ **Einstellungen**<br><sub>Sperre, Meldungen, Vorschau</sub> | [![Tray-Menü](docs/screenshots/thumbs/tray-menue.png)](docs/screenshots/tray-menue.png)<br>🔔 **Tray-Menü**<br><sub>Status, Pause, Abbrechen</sub> | |
<sub>Die Bilder entstehen mit `node scripts/make-screenshots.js` aus einem Demo-Server mit erfundenen Daten (siehe Entwicklerhandbuch).</sub>
---
## ✨ Features
@@ -24,6 +51,15 @@ Verwalte deine Repositories intuitiv ohne Kommandozeile — mit Favoriten, Verla
- 📤 **Drag & Drop Upload** — ziehe Dateien ins App-Fenster zum hochladen
- 📥 **Drag & Drop Download** — ziehe Repos raus zum herunterladen
- 🔄 **Rename & Move** — Dateien verwalten direkt im Editor
- ⏹️ **Abbrechen** — Uploads, Downloads und Batch-Clones lassen sich per Knopf oder <kbd>Esc</kbd> stoppen
- ⏱️ **Fortschritt mit MB und Restzeit** — bei großen Uploads siehst du, ob es noch läuft
- 🔁 **Nur Fehlgeschlagene wiederholen** — nach einem Teilfehler nicht alles neu hochladen
- ⏸️ **Pausieren und Fortsetzen** — Übertragungen anhalten, auch über das Tray-Menü
- 🕘 **Upload-Verlauf** — die letzten 100 Uploads, filterbar nach Ergebnis und Repo, Klick öffnet das Repo, Export als CSV
- 🛡️ **Zugangsdaten-Sperre** — `credentials.json`, `.env`, Schlüssel und Ähnliches werden auf allen Upload-Wegen nie hochgeladen
- 🔔 **Windows-Meldung bei Upload-Ende** — wenn die App im Tray versteckt oder nicht im Vordergrund ist
- 📄 **.gitignore anlegen** — Vorlagen für Node.js, .NET, Electron, Python, Java, PHP und Unity
- 📦 **Release für den Updater** — Installer wählen, Prüfsumme abgleichen, Tag prüfen, `.sha256` mit hochladen
### 📊 Versionscontrol
- 📊 **Commit-History** mit Filter nach Autor und Nachricht
@@ -38,6 +74,7 @@ Verwalte deine Repositories intuitiv ohne Kommandozeile — mit Favoriten, Verla
- 🔒 **Verschlüsselte Credentials** — alle Tokens AES-256 verschlüsselt
- 🚀 **Auto-Startup** — App optional mit Windows starten
- 🔄 **Auto-Update** — aktuelle Version automatisch verfügbar machen
- 🔍 **Kleine Symbole** — ruhige Oberfläche mit kleinen Symbolen als Standard, Größe einstellbar (klein, mittel, groß)
---
@@ -131,7 +168,7 @@ npm start
| <kbd>Ctrl</kbd>+<kbd>K</kbd> | Repo-Suche fokussieren |
| <kbd>Ctrl</kbd>+<kbd>S</kbd> | Datei im Editor speichern |
| <kbd>Ctrl</kbd>+<kbd>F</kbd> | Im Editor suchen |
| <kbd>Escape</kbd> | Suche schließen |
| <kbd>Escape</kbd> | Suche schließen · laufenden Upload/Download abbrechen (wenn kein Dialog offen ist) |
| <kbd>Rechtsklick</kbd> | Kontextmenü (Repos & Dateien) |
| <kbd>Drag</kbd> raus | Datei/Repo herunterladen |
| <kbd>Drag</kbd> rein | Datei hochladen |
@@ -176,6 +213,20 @@ Fehler gefunden oder Idee für neue Features?
## 📝 Changelog
### v2.2.1
- ⏹️ Abbrechen-Knopf (und <kbd>Esc</kbd>) für Uploads, Ordner-Downloads und Batch-Clones
- ⏸️ Pausieren und Fortsetzen im Fortschrittsbalken und im Tray-Menü
- 🔁 „Nur Fehlgeschlagene wiederholen“ bei Teilfehlern von Drag-&-Drop- und Ordner-Uploads
- ⏱️ Fortschrittsanzeige mit übertragenen MB und geschätzter Restzeit
- 🕘 Upload-Verlauf mit Filter, Klick zum Öffnen des Repos und CSV-Export
- 🛡️ Feste Sperre für Dateien mit Zugangsdaten auf allen Upload-Wegen (abschaltbar)
- 🔔 Windows-Meldung, wenn ein Upload endet, während die App nicht vorn ist
- 📄 `.gitignore`-Vorlagen im Repo, 📦 Release-Assistent für den Updater
- 🎨 Aufgeräumte Toolbar mit eigener Repo-Leiste, neues Tray-Menü mit Hover-Markierung
- 🔍 Kleine Symbole in Repo-Übersicht und Datei-Explorer als Standard (große Symbole wirken schnell erdrückend), Symbolgröße einstellbar
- ✨ Startanimation wie bei AeroMC: das Logo dreht sich, danach baut sich das Fenster von der Mitte aus auf (abschaltbar, beim Autostart im Tray nie)
- 🧪 `npm run test:ui`: Oberflächen-Testlauf
### v2.1.3 (13. Mai 2026)
- ✨ Projektname wird korrekt ausgeblendet bei Wechsel zur Übersicht
- 🐛 Bug-Fix: Zurück-Button setzt Gravur korrekt zurück
+116
View File
@@ -0,0 +1,116 @@
const test = require('node:test');
const assert = require('node:assert');
const http = require('node:http');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
require('../src/git/gitBinary.js').useBundledGit();
const { registerFeatureIpc } = require('../src/features/featuresIpc.js');
// Drei Repos; das ZIP von "langsam" kommt in kleinen Stücken über ~3 s, damit ein Abbruch mitten hineinfällt
function fakeGitea() {
const server = http.createServer((req, res) => {
const u = new URL(req.url, 'http://x');
const send = (code, obj) => { res.statusCode = code; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(obj)); };
if (u.pathname === '/api/v1/user/repos') {
if (u.searchParams.get('page') !== '1') return send(200, []);
const port = server.address().port;
return send(200, ['eins', 'langsam', 'drei'].map(name => ({
name, owner: { login: 'M_Viper' }, default_branch: 'main', empty: false,
clone_url: `http://127.0.0.1:${port}/M_Viper/${name}.git`
})));
}
const m = u.pathname.match(/^\/api\/v1\/repos\/M_Viper\/(\w+)\/archive\/main\.zip$/);
if (m) {
const total = 64 * 1024;
res.writeHead(200, { 'Content-Type': 'application/zip', 'Content-Length': String(total) });
if (m[1] !== 'langsam') return res.end(Buffer.alloc(total, 1));
let sent = 0;
const t = setInterval(() => {
if (res.destroyed) return clearInterval(t);
res.write(Buffer.alloc(2048, 1));
sent += 2048;
if (sent >= total) { clearInterval(t); res.end(); }
}, 100);
return;
}
send(404, { message: 'not found' });
});
return new Promise(r => server.listen(0, '127.0.0.1', () => r({ server, url: `http://127.0.0.1:${server.address().port}` })));
}
function setup(url) {
const handlers = {};
const events = [];
registerFeatureIpc({
ipcMain: { handle: (name, fn) => { handlers[name] = fn; } },
app: { getPath: () => fs.mkdtempSync(path.join(os.tmpdir(), 'gm-ud-')) },
readCredentials: () => ({ giteaURL: url, giteaToken: 't' }),
buildItemsFromPaths: async () => ({ items: [], skipped: [], ignored: [] }),
clearGitTransportBlock: () => false,
broadcast: (channel, p) => { if (channel === 'repo-backup-progress') events.push(p); }
});
return { call: (name, data) => handlers[name]({ sender: { send() {} } }, data), events };
}
test('Backup meldet Fortschritt: Phasen, aktuelles Repo, Bytes und Ergebnis je Repo', async () => {
const g = await fakeGitea();
try {
const { call, events } = setup(g.url);
const target = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-bk-'));
await call('repo-backup-set-config', { targetDir: target });
const r = await call('repo-backup-run-now', {});
assert.strictEqual(r.summary.ok, 3, JSON.stringify(r));
const phases = [...new Set(events.map(e => e.phase))];
assert.deepStrictEqual(phases, ['start', 'list', 'running', 'done']);
assert.deepStrictEqual(events.find(e => e.phase === 'running').repos, ['M_Viper/eins', 'M_Viper/langsam', 'M_Viper/drei']);
assert.ok(events.some(e => e.repo === 'M_Viper/langsam' && e.bytes > 0 && e.bytesTotal === 64 * 1024), 'Download-Fortschritt fehlt');
const last = events[events.length - 1];
assert.strictEqual(last.results.length, 3);
assert.ok(last.results.every(x => x.ok && x.method === 'ZIP'));
assert.ok(!events.some(e => 'abort' in e), 'interne Felder dürfen nicht nach außen gehen');
// Während kein Backup läuft, gibt es auch keinen Zwischenstand
const state = await call('repo-backup-get-config', {});
assert.strictEqual(state.running, false);
assert.strictEqual(state.progress, null);
} finally {
g.server.close();
}
});
test('Abbrechen beendet den laufenden ZIP-Download sofort und überspringt den Rest', async () => {
const g = await fakeGitea();
try {
const { call, events } = setup(g.url);
const target = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-bk-'));
await call('repo-backup-set-config', { targetDir: target });
const started = Date.now();
const running = call('repo-backup-run-now', {});
// warten, bis der langsame Download läuft
while (!events.some(e => e.repo === 'M_Viper/langsam' && e.bytes > 0)) {
if (Date.now() - started > 10000) throw new Error('Download startete nicht');
await new Promise(r => setTimeout(r, 50));
}
const state = await call('repo-backup-get-config', {});
assert.strictEqual(state.running, true);
assert.strictEqual(state.progress.repo, 'M_Viper/langsam');
assert.strictEqual((await call('repo-backup-cancel', {})).ok, true);
const r = await running;
assert.ok(Date.now() - started < 2500, 'Abbruch muss den Download sofort beenden');
assert.strictEqual(r.ok, false);
assert.strictEqual(r.summary.cancelled, true);
assert.strictEqual(r.summary.ok, 1);
assert.strictEqual(r.summary.skipped, 2);
// keine halbe Datei zurücklassen
const archiv = path.join(target, 'M_Viper', 'langsam-archiv');
assert.deepStrictEqual(fs.existsSync(archiv) ? fs.readdirSync(archiv) : [], []);
assert.strictEqual((await call('repo-backup-cancel', {})).ok, false);
} finally {
g.server.close();
}
});
+60
View File
@@ -0,0 +1,60 @@
const test = require('node:test');
const assert = require('node:assert');
const http = require('node:http');
const { deleteGiteaPathsBatched } = require('../src/git/apiHandler.js');
// Minimaler Gitea-Nachbau: Repo-Info, Trees-API (seitenweise) und Mehrfach-Commit-Endpunkt.
function fakeGitea(paths, { perPage = 1000, failPath = null } = {}) {
const files = new Map(paths.map((p, i) => [p, `sha${i}`]));
const commits = [];
const server = http.createServer((req, res) => {
const u = new URL(req.url, 'http://x');
const send = (code, obj) => { res.statusCode = code; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(obj)); };
if (req.method === 'GET' && u.pathname === '/api/v1/repos/o/r') return send(200, { default_branch: 'main' });
if (req.method === 'GET' && u.pathname === '/api/v1/repos/o/r/git/trees/main') {
const page = Number(u.searchParams.get('page') || 1);
const all = [...files.keys()].map(p => ({ path: p, type: 'blob', sha: files.get(p) }));
const slice = all.slice((page - 1) * perPage, page * perPage);
return send(200, { tree: slice, truncated: page * perPage < all.length, total_count: all.length, page });
}
if (req.method === 'POST' && u.pathname === '/api/v1/repos/o/r/contents') {
let body = '';
req.on('data', c => body += c);
req.on('end', () => {
const { files: ops } = JSON.parse(body);
if (failPath && ops.some(o => o.path === failPath)) return send(422, { message: 'kaputt' });
for (const o of ops) {
assert.strictEqual(o.operation, 'delete');
assert.strictEqual(o.sha, files.get(o.path));
files.delete(o.path);
}
commits.push(ops.length);
send(201, {});
});
return;
}
send(404, { message: 'object does not exist' });
});
return new Promise(r => server.listen(0, '127.0.0.1', () => r({ server, files, commits, url: `http://127.0.0.1:${server.address().port}` })));
}
test('Ordner wird paketweise gelöscht, Nachbarn bleiben', async () => {
const paths = [...Array.from({ length: 2500 }, (_, i) => `mobile/build/f${i}.dex`), 'mobile/build.gradle.kts', 'mobile/src/A.kt'];
const g = await fakeGitea(paths, { perPage: 1000 });
try {
const r = await deleteGiteaPathsBatched({ token: 't', url: g.url, owner: 'o', repo: 'r', prefix: 'mobile/build' });
assert.strictEqual(r.deleted, 2500);
assert.deepStrictEqual(g.commits, Array(25).fill(100));
assert.deepStrictEqual([...g.files.keys()].sort(), ['mobile/build.gradle.kts', 'mobile/src/A.kt']);
} finally { g.server.close(); }
});
test('eine fehlerhafte Datei blockiert nicht das ganze Paket', async () => {
const paths = Array.from({ length: 10 }, (_, i) => `x/f${i}`);
const g = await fakeGitea(paths, { failPath: 'x/f3' });
try {
const r = await deleteGiteaPathsBatched({ token: 't', url: g.url, owner: 'o', repo: 'r', paths });
assert.strictEqual(r.deleted, 9);
assert.deepStrictEqual(r.failed.map(f => f.path), ['x/f3']);
} finally { g.server.close(); }
});
+162
View File
@@ -0,0 +1,162 @@
const test = require('node:test');
const assert = require('node:assert');
const http = require('node:http');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
require('../src/git/gitBinary.js').useBundledGit();
const { registerFeatureIpc, parsePorcelainZ } = require('../src/features/featuresIpc.js');
const { scanForSecrets, checkName } = require('../src/features/secretScan.js');
// Minimaler Gitea-Nachbau für Diagnose, PRs/Issues und Backup
function fakeGitea() {
const calls = [];
const comments = [];
const server = http.createServer((req, res) => {
const u = new URL(req.url, 'http://x');
calls.push(`${req.method} ${u.pathname}`);
const send = (code, obj) => { res.statusCode = code; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(obj)); };
let body = '';
req.on('data', c => body += c);
req.on('end', () => {
const p = u.pathname;
if (p === '/api/v1/version') return send(200, { version: '1.24.0' });
if (p === '/api/v1/user') return send(200, { login: 'M_Viper' });
if (p === '/api/v1/user/repos') {
if (u.searchParams.get('page') && u.searchParams.get('page') !== '1') return send(200, []);
return send(200, [{ name: 'deskhub', owner: { login: 'M_Viper' }, default_branch: 'main', empty: false,
clone_url: `http://127.0.0.1:${server.address().port}/M_Viper/deskhub.git` }]);
}
if (p === '/api/v1/repos/M_Viper/deskhub/pulls') return send(200, [{ number: 3, title: 'Fix', state: 'open', user: { login: 'a' }, labels: [], head: { label: 'fix' }, base: { label: 'main' }, mergeable: true }]);
if (p === '/api/v1/repos/M_Viper/deskhub/issues' && req.method === 'GET') return send(200, [{ number: 5, title: 'Bug', state: 'open', labels: [{ name: 'bug', color: 'ff0000' }] }]);
if (p === '/api/v1/repos/M_Viper/deskhub/issues' && req.method === 'POST') return send(201, { number: 6, ...JSON.parse(body), state: 'open', labels: [] });
if (p === '/api/v1/repos/M_Viper/deskhub/issues/5/comments' && req.method === 'POST') { comments.push(JSON.parse(body).body); return send(201, {}); }
if (p === '/api/v1/repos/M_Viper/deskhub/issues/5/comments') return send(200, comments.map((b, i) => ({ id: i, body: b, user: { login: 'M_Viper' } })));
if (p === '/api/v1/repos/M_Viper/deskhub/issues/5') return send(200, { number: 5, title: 'Bug', state: 'open', labels: [] });
if (p === '/api/v1/repos/M_Viper/deskhub/pulls/3/merge') return send(200, {});
if (p === '/api/v1/repos/M_Viper/deskhub/archive/main.zip') {
res.statusCode = 200; res.setHeader('Content-Type', 'application/zip'); return res.end(Buffer.from('PK\u0003\u0004fake'));
}
send(404, { message: 'not found' });
});
});
return new Promise(r => server.listen(0, '127.0.0.1', () => r({ server, calls, comments, url: `http://127.0.0.1:${server.address().port}` })));
}
function setup(url) {
const handlers = {};
const userData = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-ud-'));
registerFeatureIpc({
ipcMain: { handle: (name, fn) => { handlers[name] = fn; } },
app: { getPath: () => userData },
readCredentials: () => ({ giteaURL: url, giteaToken: 'geheim123' }),
buildItemsFromPaths: async (paths) => ({
items: paths.map(p => ({ localFile: p, targetPath: path.basename(p) })), skipped: [], ignored: []
}),
clearGitTransportBlock: () => false
});
const call = (name, data) => handlers[name]({ sender: { send() {} } }, data);
return { call, userData };
}
test('Geheimnisse werden an Name und Inhalt erkannt, Vorlagen nicht', async () => {
assert.ok(checkName('google-services.json'));
assert.ok(checkName('.env'));
assert.ok(checkName('release.keystore'));
assert.strictEqual(checkName('.env.example'), null);
assert.strictEqual(checkName('README.md'), null);
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-sec-'));
const bot = path.join(dir, 'bot.js');
fs.writeFileSync(bot, 'const TOKEN = "123456789:AA' + 'b'.repeat(33) + '";');
const clean = path.join(dir, 'app.js');
fs.writeFileSync(clean, 'console.log("hallo")');
const found = await scanForSecrets([{ localFile: bot, size: fs.statSync(bot).size }, { localFile: clean, size: fs.statSync(clean).size }]);
assert.match(found.get(bot), /Telegram/);
assert.strictEqual(found.has(clean), false);
fs.rmSync(dir, { recursive: true, force: true });
});
test('git status -z wird korrekt gelesen (inkl. Umbenennung)', () => {
const files = parsePorcelainZ(' M src/a.js\0?? neu.txt\0R neu/name.js\0alt/name.js\0D weg.txt\0');
assert.deepStrictEqual(files.map(f => [f.code, f.path, f.from]), [['M', 'src/a.js', null], ['?', 'neu.txt', null], ['R', 'neu/name.js', 'alt/name.js'], ['D', 'weg.txt', null]]);
});
test('Lokale Änderungen: Status, Diff und Commit nur der ausgewählten Dateien', async () => {
const { call } = setup('http://127.0.0.1:1');
const repo = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-local-'));
const git = (...a) => spawnSync('git', ['-C', repo, '-c', 'user.name=t', '-c', 'user.email=t@t', ...a], { encoding: 'utf8' });
git('init', '-q');
fs.writeFileSync(path.join(repo, 'a.txt'), 'eins\n');
git('add', '.'); git('commit', '-qm', 'start');
fs.writeFileSync(path.join(repo, 'a.txt'), 'zwei\n');
fs.writeFileSync(path.join(repo, 'b.txt'), 'neu\n');
const st = await call('local-git', { action: 'status', folder: repo });
assert.strictEqual(st.ok, true);
assert.deepStrictEqual(st.files.map(f => f.path).sort(), ['a.txt', 'b.txt']);
const diff = await call('local-git', { action: 'diff', folder: repo, file: 'a.txt', code: 'M' });
assert.match(diff.diff, /-eins/);
assert.match(diff.diff, /\+zwei/);
const c = await call('local-git', { action: 'commit', folder: repo, files: ['a.txt'], message: 'nur a' });
assert.strictEqual(c.ok, true, c.error);
const after = await call('local-git', { action: 'status', folder: repo });
assert.deepStrictEqual(after.files.map(f => f.path), ['b.txt']);
const notRepo = await call('local-git', { action: 'status', folder: os.tmpdir() + path.sep + 'gm-gibtsnicht-' + Date.now() });
assert.strictEqual(notRepo.ok, false);
fs.rmSync(repo, { recursive: true, force: true });
});
test('Upload-Vorschau markiert Geheimnisse', async () => {
const { call } = setup('http://127.0.0.1:1');
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-prev-'));
const a = path.join(dir, 'main.kt'); fs.writeFileSync(a, 'fun main() {}');
const g = path.join(dir, 'google-services.json'); fs.writeFileSync(g, '{}');
const r = await call('preview-upload-paths', { paths: [a, g], destPath: '' });
assert.strictEqual(r.ok, true);
assert.strictEqual(r.secretCount, 1);
assert.ok(r.files.find(f => f.localFile === g).secret);
fs.rmSync(dir, { recursive: true, force: true });
});
test('Diagnose, PRs/Issues und Backup gegen einen nachgebauten Server', async () => {
const g = await fakeGitea();
const { call } = setup(g.url);
try {
const d = await call('run-server-diagnosis', {});
const byLabel = Object.fromEntries(d.checks.map(c => [c.label, c.status]));
assert.strictEqual(byLabel['Gitea-API'], 'ok');
assert.strictEqual(byLabel['Token'], 'ok');
assert.strictEqual(byLabel['Erreichbar über IPv4'], 'ok');
assert.ok(!JSON.stringify(d).includes('geheim123'), 'Token darf nicht in der Diagnose stehen');
const pulls = await call('gitea-collab', { action: 'listPulls', owner: 'M_Viper', repo: 'deskhub' });
assert.strictEqual(pulls.items[0].head, 'fix');
const issues = await call('gitea-collab', { action: 'listIssues', owner: 'M_Viper', repo: 'deskhub' });
assert.strictEqual(issues.items[0].labels[0].name, 'bug');
assert.strictEqual((await call('gitea-collab', { action: 'comment', owner: 'M_Viper', repo: 'deskhub', index: 5, body: 'Danke' })).ok, true);
const detail = await call('gitea-collab', { action: 'get', owner: 'M_Viper', repo: 'deskhub', index: 5 });
assert.strictEqual(detail.comments[0].body, 'Danke');
assert.strictEqual((await call('gitea-collab', { action: 'merge', owner: 'M_Viper', repo: 'deskhub', index: 3, style: 'squash' })).ok, true);
assert.strictEqual((await call('gitea-collab', { action: 'createIssue', owner: 'M_Viper', repo: 'deskhub', title: 'Neu' })).item.number, 6);
const target = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-bk-'));
await call('repo-backup-set-config', { targetDir: target, interval: 'daily', keep: 2 });
for (let i = 0; i < 3; i++) {
const r = await call('repo-backup-run-now', {});
assert.strictEqual(r.summary.ok, 1, JSON.stringify(r));
await new Promise(res => setTimeout(res, 61000 / 60)); // eindeutige Zeitstempel
}
const zips = fs.readdirSync(path.join(target, 'M_Viper', 'deskhub-archiv'));
assert.ok(zips.length <= 3 && zips.length >= 1);
const state = await call('repo-backup-get-config', {});
assert.strictEqual(state.config.lastResult.total, 1);
fs.rmSync(target, { recursive: true, force: true });
} finally {
g.server.close();
}
});
+33
View File
@@ -0,0 +1,33 @@
const test = require('node:test');
const assert = require('node:assert');
const { listTemplates, buildGitignore } = require('../src/features/gitignoreTemplates.js');
test('listTemplates nennt alle Vorlagen mit Beschriftung', () => {
const list = listTemplates();
assert.deepStrictEqual(list.map(t => t.key), ['node', 'dotnet', 'electron', 'python', 'java', 'php', 'unity']);
assert.ok(list.every(t => t.label));
});
test('buildGitignore: Vorlagen für Python, Java, PHP und Unity enthalten ihre Kerneinträge', () => {
assert.match(buildGitignore(['python']), /__pycache__\//);
assert.match(buildGitignore(['java']), /target\//);
assert.match(buildGitignore(['php']), /vendor\//);
assert.match(buildGitignore(['unity']), /\[Ll\]ibrary\//);
});
test('buildGitignore enthält immer den allgemeinen Teil', () => {
const out = buildGitignore([]);
assert.match(out, /\.env/);
assert.doesNotMatch(out, /node_modules/);
});
test('buildGitignore fügt gewählte Vorlagen genau einmal hinzu', () => {
const out = buildGitignore(['node', 'node', 'electron']);
assert.strictEqual(out.split('node_modules/').length - 1, 1);
assert.match(out, /dist\//);
assert.doesNotMatch(out, /\.vs\//);
});
test('buildGitignore ignoriert unbekannte Schlüssel', () => {
assert.doesNotThrow(() => buildGitignore(['gibt-es-nicht', '__proto__', null]));
});
+41
View File
@@ -0,0 +1,41 @@
const test = require('node:test');
const assert = require('node:assert');
const http = require('node:http');
const { bareHostname } = require('../src/utils/helpers.js');
const { checkGiteaConnection } = require('../src/git/apiHandler.js');
function listen(host) {
return new Promise((resolve, reject) => {
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ version: '1.22.0' }));
});
server.once('error', reject);
server.listen(0, host, () => resolve(server));
});
}
test('bareHostname entfernt Klammern nur bei IPv6-Literalen', () => {
assert.strictEqual(bareHostname(new URL('http://[2001:db8::1]:3000').hostname), '2001:db8::1');
assert.strictEqual(bareHostname('[::1]'), '::1');
assert.strictEqual(bareHostname('127.0.0.1'), '127.0.0.1');
assert.strictEqual(bareHostname('git.example.com'), 'git.example.com');
assert.strictEqual(bareHostname(undefined), '');
});
test('Gitea-Verbindung über IPv4', async () => {
const server = await listen('127.0.0.1');
try {
const r = await checkGiteaConnection({ url: `http://127.0.0.1:${server.address().port}` });
assert.strictEqual(r.ok, true);
} finally { server.close(); }
});
test('Gitea-Verbindung über IPv6', async (t) => {
let server;
try { server = await listen('::1'); } catch { return t.skip('IPv6-Loopback nicht verfügbar'); }
try {
const r = await checkGiteaConnection({ url: `http://[::1]:${server.address().port}` });
assert.strictEqual(r.ok, true);
} finally { server.close(); }
});
+77
View File
@@ -0,0 +1,77 @@
const test = require('node:test');
const assert = require('node:assert');
const { createPauseGate } = require('../src/features/pauseGate.js');
const tick = () => new Promise((r) => setTimeout(r, 20));
test('Ohne Pause läuft wait() sofort durch', async () => {
const gate = createPauseGate();
await gate.wait();
await gate.wait(new AbortController().signal);
assert.strictEqual(gate.isPaused(), false);
});
test('Während der Pause bleibt wait() stehen und läuft nach resume() weiter', async () => {
const gate = createPauseGate();
gate.pause();
let done = false;
const p = gate.wait().then(() => { done = true; });
await tick();
assert.strictEqual(done, false);
assert.strictEqual(gate.waitingCount(), 1);
gate.resume();
await p;
assert.strictEqual(done, true);
assert.strictEqual(gate.waitingCount(), 0);
});
test('resume() gibt alle Wartenden frei', async () => {
const gate = createPauseGate();
gate.pause();
const all = Promise.all([gate.wait(), gate.wait(), gate.wait()]);
await tick();
assert.strictEqual(gate.waitingCount(), 3);
gate.resume();
await all;
});
test('Abbruch während der Pause löst wait() mit dem Abbruchfehler aus', async () => {
const gate = createPauseGate();
gate.pause();
const c = new AbortController();
const p = gate.wait(c.signal);
await tick();
c.abort();
await assert.rejects(p, (e) => e.code === 'UPLOAD_CANCELLED');
assert.strictEqual(gate.waitingCount(), 0);
});
test('Bereits abgebrochenes Signal wirft sofort, auch ohne Pause', async () => {
const gate = createPauseGate();
const c = new AbortController();
c.abort();
await assert.rejects(gate.wait(c.signal), (e) => e.code === 'UPLOAD_CANCELLED');
});
test('onChange meldet nur echte Wechsel', () => {
const states = [];
const gate = createPauseGate({ onChange: (p) => states.push(p) });
assert.strictEqual(gate.pause(), true);
assert.strictEqual(gate.pause(), false);
assert.strictEqual(gate.resume(), true);
assert.strictEqual(gate.resume(), false);
assert.deepStrictEqual(states, [true, false]);
});
test('Ein abgebrochener Wartender blockiert die übrigen nicht', async () => {
const gate = createPauseGate();
gate.pause();
const c = new AbortController();
const a = gate.wait(c.signal).catch((e) => e.code);
const b = gate.wait();
await tick();
c.abort();
assert.strictEqual(await a, 'UPLOAD_CANCELLED');
gate.resume();
await b;
});
+76
View File
@@ -0,0 +1,76 @@
const test = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { createPrefs, DEFAULTS } = require('../src/features/appPrefs.js');
const { useBundledGit, applyGitIdentity, commitIdentity } = require('../src/git/gitBinary.js');
const { createRepoBackup, excludeMatcher } = require('../src/backup/GiteaRepoBackup.js');
const tmp = (p) => fs.mkdtempSync(path.join(os.tmpdir(), p));
test('Einstellungen: Standardwerte, Bereinigung und ungültige E-Mail', () => {
const file = path.join(tmp('gm-prefs-'), 'app-settings.json');
const prefs = createPrefs(file);
assert.deepStrictEqual(prefs.get(), DEFAULTS);
const r = prefs.set({ zoom: 400, theme: 'pink', closeAction: 'tray', gitName: 'Max <x>\n', unbekannt: 1, uploadSecretScan: 0 });
assert.strictEqual(r.prefs.zoom, 150);
assert.strictEqual(r.prefs.theme, 'dark');
assert.strictEqual(r.prefs.closeAction, 'tray');
assert.strictEqual(r.prefs.gitName, 'Max x');
assert.strictEqual(r.prefs.uploadSecretScan, false);
assert.ok(!('unbekannt' in r.prefs));
const bad = prefs.set({ gitEmail: 'keine-mail' });
assert.deepStrictEqual(bad.rejected, ['gitEmail']);
assert.strictEqual(bad.prefs.gitEmail, '');
// nach dem Neuladen noch da
assert.strictEqual(createPrefs(file).get().closeAction, 'tray');
});
test('Git-Identität greift nur als Rückfallwert und verschwindet beim Leeren', () => {
useBundledGit();
const userData = tmp('gm-ud-');
const home = tmp('gm-home-');
const env = { ...process.env, HOME: home, USERPROFILE: home };
const ident = () => spawnSync('git', ['var', 'GIT_AUTHOR_IDENT'], { encoding: 'utf8', env: { ...env, GIT_CONFIG_SYSTEM: process.env.GIT_CONFIG_SYSTEM || '' } }).stdout;
assert.strictEqual(applyGitIdentity({ name: 'Test Name', email: 'test@example.com' }, userData), true);
assert.match(ident(), /^Test Name <test@example\.com>/);
// Eine globale Einstellung hat Vorrang
fs.writeFileSync(path.join(home, '.gitconfig'), '[user]\n\tname = Global\n\temail = global@example.com\n');
assert.match(ident(), /^Global <global@example\.com>/);
applyGitIdentity({ name: '', email: '' }, userData);
assert.strictEqual(process.env.GIT_CONFIG_SYSTEM, undefined);
assert.ok(!fs.existsSync(path.join(userData, 'git-system.gitconfig')));
assert.deepStrictEqual(commitIdentity({ gitName: '', gitEmail: '' }), { name: 'Git Manager GUI', email: 'gui@gitmanager.local' });
});
test('Backup: Ausschlussmuster und Zeitpunkt für automatische Backups', () => {
const m = excludeMatcher('test-*\nM_Viper/Alt?\n# Kommentar\nmy.repo');
assert.strictEqual(m('M_Viper/test-1'), true);
assert.strictEqual(m('M_Viper/Alt1'), true);
assert.strictEqual(m('M_Viper/Alt12'), false);
assert.strictEqual(m('x/my.repo'), true);
assert.strictEqual(m('x/myXrepo'), false);
const configPath = path.join(tmp('gm-bk-'), 'repo-backup.json');
const backup = createRepoBackup({ configPath, getContext() {}, giteaCall() {}, runGit() {}, isPackFailure() {}, redact: (s) => s });
backup.setConfig({ targetDir: 'X:/irgendwo', interval: 'daily', time: '03:00' });
const at = (s) => new Date(s);
// noch nie gelaufen: vor 03:00 nicht, danach schon
assert.strictEqual(backup.isDue(at('2026-10-22T02:59:00')), true, 'Slot von gestern 03:00 ist offen');
fs.writeFileSync(configPath, JSON.stringify({ ...JSON.parse(fs.readFileSync(configPath, 'utf8')), lastRun: '2026-10-22T03:05:00' }));
assert.strictEqual(backup.isDue(at('2026-10-22T20:00:00')), false, 'heute schon gelaufen');
assert.strictEqual(backup.isDue(at('2026-10-23T02:59:00')), false, 'vor 03:00 noch nicht');
assert.strictEqual(backup.isDue(at('2026-10-23T03:00:00')), true, 'ab 03:00 wieder fällig');
backup.setConfig({ time: '25:99' });
assert.strictEqual(backup.getState().config.time, '');
});
+64
View File
@@ -0,0 +1,64 @@
const test = require('node:test');
const assert = require('node:assert');
const {
parseVersionFromFileName, compareVersions, parseSidecar, checkNewTag, buildReleaseBody
} = require('../src/features/releaseAssist.js');
const HASH = '5ca7f0c680b6d73a92eadb5e90f8164f7f3fa52071ffaac54803b0e5ec95f35d';
test('Version aus dem Dateinamen', () => {
assert.strictEqual(parseVersionFromFileName('Git-Manager-GUI-Setup-2.2.1.exe'), '2.2.1');
assert.strictEqual(parseVersionFromFileName('Git Manager GUI Setup 2.1.10.exe'), '2.1.10');
assert.strictEqual(parseVersionFromFileName('setup.exe'), null);
assert.strictEqual(parseVersionFromFileName(null), null);
});
test('Versionen werden numerisch verglichen, nicht als Text', () => {
assert.strictEqual(compareVersions('2.2.1', '2.2.0'), 1);
assert.strictEqual(compareVersions('v2.10.0', 'v2.9.9'), 1);
assert.strictEqual(compareVersions('2.2.1', 'v2.2.1'), 0);
assert.strictEqual(compareVersions('2.1.9', '2.2.0'), -1);
assert.deepStrictEqual(['v2.10.0', 'v2.9.0', 'v2.2.1'].sort(compareVersions), ['v2.2.1', 'v2.9.0', 'v2.10.0']);
});
test('Sidecar-Datei: Hash wird gefunden, auch mit Großbuchstaben', () => {
assert.strictEqual(parseSidecar(`${HASH} Git-Manager-GUI-Setup-2.2.1.exe\n`), HASH);
assert.strictEqual(parseSidecar(HASH.toUpperCase()), HASH);
assert.strictEqual(parseSidecar('kein Hash'), null);
assert.strictEqual(parseSidecar('abc123'), null);
});
test('Neuer Tag: Format, Duplikat und Reihenfolge werden geprüft', () => {
const releases = [{ tag_name: 'v2.2.0' }, { tag_name: 'v2.1.9' }];
assert.strictEqual(checkNewTag('v2.2.1', releases).ok, true);
assert.strictEqual(checkNewTag('2.2.1', releases).ok, true);
assert.strictEqual(checkNewTag('v2.10.0', releases).ok, true);
assert.match(checkNewTag('release-2', releases).reason, /Version wie v2\.2\.1/);
assert.match(checkNewTag('v2.2.0', releases).reason, /gibt es schon/);
assert.match(checkNewTag('V2.2.0', releases).reason, /gibt es schon/);
assert.match(checkNewTag('v2.1.5', releases).reason, /nicht neuer/);
});
test('Neuer Tag: ohne vorhandene Releases ist jede Version erlaubt', () => {
assert.strictEqual(checkNewTag('v1.0.0', []).ok, true);
assert.strictEqual(checkNewTag('v1.0.0', null).ok, true);
assert.strictEqual(checkNewTag('v1.0.0', [{ tag_name: 'nightly' }]).ok, true);
});
test('Release-Text: Prüfsummen im Format, das der Updater liest', () => {
const body = buildReleaseBody('## Neu\n\n- Abbrechen\n', [{ name: 'Git-Manager-GUI-Setup-2.2.1.exe', sha256: HASH.toUpperCase() }]);
assert.match(body, /^## Neu/);
assert.ok(body.includes(`- Git-Manager-GUI-Setup-2.2.1.exe: ${HASH}`));
// genau so liest updater.js die Zeile
const line = body.split('\n').find((l) => l.toLowerCase().includes('git-manager-gui-setup-2.2.1.exe') && /\b[a-f0-9]{64}\b/i.test(l));
assert.ok(line);
assert.strictEqual(line.match(/\b([a-fA-F0-9]{64})\b/)[1], HASH);
});
test('Release-Text: ungültige Prüfsummen und leere Notizen', () => {
assert.strictEqual(buildReleaseBody('Nur Text', [{ name: 'a.exe', sha256: 'zu kurz' }]), 'Nur Text');
assert.strictEqual(buildReleaseBody('', []), '');
assert.ok(buildReleaseBody('', [{ name: 'a.exe', sha256: HASH }]).startsWith('### Prüfsummen'));
});
+201
View File
@@ -0,0 +1,201 @@
const test = require('node:test');
const assert = require('node:assert');
const http = require('node:http');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const archiver = require('archiver');
require('../src/git/gitBinary.js').useBundledGit();
const { registerFeatureIpc } = require('../src/features/featuresIpc.js');
const git = (cwd, ...a) => spawnSync('git', ['-c', 'user.name=t', '-c', 'user.email=t@t', ...a], { cwd, encoding: 'utf8' });
const tmp = (p) => fs.mkdtempSync(path.join(os.tmpdir(), p));
function makeZip(file, entries) {
return new Promise((resolve, reject) => {
const out = fs.createWriteStream(file);
const zip = archiver('zip');
out.on('close', resolve);
zip.on('error', reject);
zip.pipe(out);
for (const [name, content] of entries) zip.append(content, { name });
zip.finalize();
});
}
// Backup-Ordner wie von GiteaRepoBackup angelegt: Spiegel + ZIP-Stände
async function makeBackupDir() {
const work = tmp('gm-src-');
git(work, 'init', '-q', '-b', 'main');
fs.writeFileSync(path.join(work, 'a.txt'), 'hallo');
git(work, 'add', '.'); git(work, 'commit', '-qm', 'eins');
git(work, 'branch', 'feature'); git(work, 'tag', 'v1.0');
const dir = tmp('gm-bk-');
fs.mkdirSync(path.join(dir, 'M_Viper'));
git(dir, 'clone', '-q', '--mirror', work, path.join(dir, 'M_Viper', 'deskhub.git'));
const zipDir = path.join(dir, 'M_Viper', 'deskhub-archiv');
fs.mkdirSync(zipDir);
await makeZip(path.join(zipDir, 'deskhub-2026-10-01_10-00.zip'), [['deskhub/a.txt', 'alt'], ['deskhub/src/b.kt', 'fun b() {}']]);
await makeZip(path.join(zipDir, 'deskhub-2026-10-20_10-00.zip'), [['deskhub/a.txt', 'neu'], ['deskhub/src/b.kt', 'fun b() {}']]);
fs.utimesSync(path.join(zipDir, 'deskhub-2026-10-01_10-00.zip'), new Date('2026-10-01'), new Date('2026-10-01'));
fs.writeFileSync(path.join(dir, 'backup-protokoll.json'), '{}');
return { dir, work };
}
// Gitea-Nachbau: neue Repos zeigen auf echte lokale Bare-Repos, damit der Push wirklich läuft
function fakeGitea() {
const created = [];
const patches = [];
const repoRoot = tmp('gm-remote-');
const server = http.createServer((req, res) => {
const u = new URL(req.url, 'http://x');
const send = (code, obj) => { res.statusCode = code; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(obj)); };
let body = '';
req.on('data', c => body += c);
req.on('end', () => {
if (u.pathname === '/api/v1/user') return send(200, { login: 'M_Viper' });
if (u.pathname === '/api/v1/user/repos' && req.method === 'POST') {
const b = JSON.parse(body);
if (b.name === 'deskhub') return send(409, { message: 'repository already exists' });
const bare = path.join(repoRoot, `${b.name}.git`);
spawnSync('git', ['init', '-q', '--bare', '-b', 'main', bare]);
created.push(b);
return send(201, { name: b.name, full_name: `M_Viper/${b.name}`, owner: { login: 'M_Viper' }, clone_url: bare, html_url: `http://gitea/M_Viper/${b.name}`, default_branch: 'main' });
}
if (u.pathname.startsWith('/api/v1/repos/M_Viper/') && req.method === 'PATCH') { patches.push(JSON.parse(body)); return send(200, {}); }
send(404, { message: 'not found' });
});
});
return new Promise(r => server.listen(0, '127.0.0.1', () => r({ server, created, patches, repoRoot, url: `http://127.0.0.1:${server.address().port}` })));
}
function setup(url) {
const handlers = {};
const userData = tmp('gm-ud-');
registerFeatureIpc({
ipcMain: { handle: (name, fn) => { handlers[name] = fn; } },
app: { getPath: () => userData },
readCredentials: () => ({ giteaURL: url, giteaToken: 'geheim123' }),
buildItemsFromPaths: async () => ({ items: [], skipped: [], ignored: [] }),
clearGitTransportBlock: () => false
});
return (name, data) => handlers[name]({ sender: { send() {} } }, data);
}
test('Sicherungen werden gefunden: Spiegel und ZIP-Stände, neuester zuerst', async () => {
const { dir } = await makeBackupDir();
const call = setup('http://127.0.0.1:1');
const r = await call('repo-backup-list', { dir });
assert.strictEqual(r.ok, true, r.error);
assert.strictEqual(r.backups.length, 1);
const b = r.backups[0];
assert.deepStrictEqual([b.owner, b.repo], ['M_Viper', 'deskhub']);
assert.ok(b.mirror);
assert.deepStrictEqual(b.zips.map(z => z.file), ['deskhub-2026-10-20_10-00.zip', 'deskhub-2026-10-01_10-00.zip']);
});
test('Lokal: Spiegel wird zur Arbeitskopie mit allen Branches, ZIP ohne Oberordner entpackt', async () => {
const { dir } = await makeBackupDir();
const call = setup('http://127.0.0.1:1');
const { backups: [b] } = await call('repo-backup-list', { dir });
const dest1 = tmp('gm-dst-');
const m = await call('repo-restore-local', { kind: 'mirror', source: b.mirror.path, destParent: dest1, repo: 'deskhub' });
assert.strictEqual(m.ok, true, m.error);
assert.strictEqual(fs.readFileSync(path.join(dest1, 'deskhub', 'a.txt'), 'utf8'), 'hallo');
assert.match(git(path.join(dest1, 'deskhub'), 'branch', '-r').stdout, /feature/);
// Zweiter Versuch in denselben Ordner darf nichts überschreiben
const again = await call('repo-restore-local', { kind: 'mirror', source: b.mirror.path, destParent: dest1, repo: 'deskhub' });
assert.strictEqual(again.ok, false);
assert.match(again.error, /existiert bereits/);
const dest2 = tmp('gm-dst-');
const z = await call('repo-restore-local', { kind: 'zip', source: b.zips[0].path, destParent: dest2, repo: 'deskhub' });
assert.strictEqual(z.ok, true, z.error);
assert.strictEqual(z.files, 2);
assert.strictEqual(fs.readFileSync(path.join(dest2, 'deskhub', 'a.txt'), 'utf8'), 'neu');
assert.ok(fs.existsSync(path.join(dest2, 'deskhub', 'src', 'b.kt')));
});
// ZIP von Hand bauen: archiver entschärft "../" schon beim Erzeugen, ein echter Angreifer nicht.
function rawZip(file, entries) {
const zlib = require('node:zlib');
const locals = [];
const centrals = [];
let offset = 0;
for (const [name, content] of entries) {
const n = Buffer.from(name);
const data = Buffer.from(content);
const crc = zlib.crc32(data);
const lh = Buffer.alloc(30);
lh.writeUInt32LE(0x04034b50, 0); lh.writeUInt16LE(20, 4); lh.writeUInt32LE(crc, 14);
lh.writeUInt32LE(data.length, 18); lh.writeUInt32LE(data.length, 22); lh.writeUInt16LE(n.length, 26);
const ch = Buffer.alloc(46);
ch.writeUInt32LE(0x02014b50, 0); ch.writeUInt16LE(20, 4); ch.writeUInt16LE(20, 6); ch.writeUInt32LE(crc, 16);
ch.writeUInt32LE(data.length, 20); ch.writeUInt32LE(data.length, 24); ch.writeUInt16LE(n.length, 28); ch.writeUInt32LE(offset, 42);
locals.push(lh, n, data);
centrals.push(ch, n);
offset += lh.length + n.length + data.length;
}
const cd = Buffer.concat(centrals);
const end = Buffer.alloc(22);
end.writeUInt32LE(0x06054b50, 0); end.writeUInt16LE(entries.length, 8); end.writeUInt16LE(entries.length, 10);
end.writeUInt32LE(cd.length, 12); end.writeUInt32LE(offset, 16);
fs.writeFileSync(file, Buffer.concat([...locals, cd, end]));
}
test('Archive mit Pfaden außerhalb des Ziels werden abgewiesen', async () => {
const d = tmp('gm-evil-');
const zip = path.join(d, 'evil.zip');
rawZip(zip, [['deskhub/ok.txt', 'ok'], ['deskhub/../../boese.txt', 'x']]);
const unzipper = require('unzipper');
const names = (await unzipper.Open.file(zip)).files.map(f => f.path);
assert.ok(names.includes('deskhub/../../boese.txt'), 'Testarchiv muss den gefährlichen Pfad wirklich enthalten');
const call = setup('http://127.0.0.1:1');
const destParent = tmp('gm-dst-');
const r = await call('repo-restore-local', { kind: 'zip', source: zip, destParent, repo: 'x' });
assert.strictEqual(r.ok, false);
assert.match(r.error, /Unsicherer Pfad/);
assert.ok(!fs.existsSync(path.join(destParent, 'x', 'ok.txt')), 'auch harmlose Dateien dürfen dann nicht entpackt werden');
assert.ok(!fs.existsSync(path.join(destParent, 'boese.txt')));
});
test('Gitea: Spiegel und ZIP als neues Repo, Namenskonflikt und ZIP-Überschreiben werden abgefangen', async () => {
const { dir } = await makeBackupDir();
const g = await fakeGitea();
try {
const call = setup(g.url);
const { backups: [b] } = await call('repo-backup-list', { dir });
const m = await call('repo-restore-gitea', { kind: 'mirror', source: b.mirror.path, owner: 'M_Viper', repo: 'deskhub', mode: 'new', newName: 'deskhub-wieder', isPrivate: true });
assert.strictEqual(m.ok, true, m.error);
const bare = path.join(g.repoRoot, 'deskhub-wieder.git');
const refs = git(bare, 'for-each-ref', '--format=%(refname)').stdout;
assert.match(refs, /refs\/heads\/main/);
assert.match(refs, /refs\/heads\/feature/);
assert.match(refs, /refs\/tags\/v1\.0/);
assert.strictEqual(g.created[0].private, true);
const z = await call('repo-restore-gitea', { kind: 'zip', source: b.zips[0].path, owner: 'M_Viper', repo: 'deskhub', mode: 'new', newName: 'deskhub-zip' });
assert.strictEqual(z.ok, true, z.error);
assert.strictEqual(z.method, 'Git');
const files = git(path.join(g.repoRoot, 'deskhub-zip.git'), 'ls-tree', '-r', '--name-only', 'main').stdout.trim().split('\n');
assert.deepStrictEqual(files.sort(), ['a.txt', 'src/b.kt']);
const conflict = await call('repo-restore-gitea', { kind: 'mirror', source: b.mirror.path, owner: 'M_Viper', repo: 'deskhub', mode: 'new', newName: 'deskhub' });
assert.strictEqual(conflict.ok, false);
assert.match(conflict.error, /existiert bereits/);
const zipOverwrite = await call('repo-restore-gitea', { kind: 'zip', source: b.zips[0].path, owner: 'M_Viper', repo: 'deskhub', mode: 'overwrite' });
assert.strictEqual(zipOverwrite.ok, false);
assert.match(zipOverwrite.error, /nur mit einem Git-Spiegel/);
const badName = await call('repo-restore-gitea', { kind: 'zip', source: b.zips[0].path, owner: 'M_Viper', repo: 'deskhub', mode: 'new', newName: 'mit leerzeichen' });
assert.strictEqual(badName.ok, false);
} finally {
g.server.close();
}
});
+36
View File
@@ -0,0 +1,36 @@
const test = require('node:test');
const assert = require('node:assert');
const { remainingMs, centeredBounds, MIN_SPIN_MS } = require('../src/features/splash.js');
test('Startanimation: wurde das Logo nie gezeigt, gibt es keine Wartezeit', () => {
assert.strictEqual(remainingMs(0, 5000, 1600), 0);
});
test('Startanimation: das Logo dreht sich mindestens minMs', () => {
assert.strictEqual(remainingMs(1000, 1300, 1600), 1300);
assert.strictEqual(remainingMs(1000, 1000, 1600), 1600);
});
test('Startanimation: nach Ablauf von minMs wird nicht mehr gewartet', () => {
assert.strictEqual(remainingMs(1000, 2600, 1600), 0);
assert.strictEqual(remainingMs(1000, 9000, 1600), 0);
});
test('Startanimation: Mindestdauer ist wie bei AeroMC 1,6 s', () => {
assert.strictEqual(MIN_SPIN_MS, 1600);
});
test('Startanimation: Fenster wird im Arbeitsbereich zentriert', () => {
const b = centeredBounds({ x: 0, y: 0, width: 1920, height: 1040 }, 1200, 820);
assert.deepStrictEqual(b, { x: 360, y: 110, width: 1200, height: 820 });
});
test('Startanimation: Arbeitsbereich mit Versatz und mehrere Monitore', () => {
const b = centeredBounds({ x: -1920, y: 40, width: 1920, height: 1000 }, 1200, 820);
assert.deepStrictEqual(b, { x: -1560, y: 130, width: 1200, height: 820 });
});
test('Startanimation: auf kleinen Bildschirmen wird das Fenster begrenzt', () => {
const b = centeredBounds({ x: 0, y: 0, width: 1024, height: 700 }, 1200, 820);
assert.deepStrictEqual(b, { x: 0, y: 0, width: 1024, height: 700 });
});
+28
View File
@@ -0,0 +1,28 @@
const test = require('node:test');
const assert = require('node:assert');
const { describeTransfer } = require('../src/features/transferNotice.js');
test('Erfolg nennt Repo und Dateizahl', () => {
assert.deepStrictEqual(describeTransfer({ status: 'ok', repo: 'a/b', files: 3 }), { title: 'Upload fertig', body: 'a/b: 3 Dateien hochgeladen.' });
assert.match(describeTransfer({ status: 'ok', repo: 'a/b', files: 1 }).body, /1 Datei hochgeladen/);
});
test('Erfolg ohne bekannte Dateizahl bleibt lesbar', () => {
assert.strictEqual(describeTransfer({ status: 'ok', repo: 'a/b', files: null }).body, 'a/b: Upload hochgeladen.');
});
test('Abgebrochene Uploads melden nichts', () => {
assert.strictEqual(describeTransfer({ status: 'abgebrochen', repo: 'a/b' }), null);
assert.strictEqual(describeTransfer(null), null);
});
test('Fehler zeigt fehlgeschlagene Dateien oder den Fehlertext', () => {
assert.strictEqual(describeTransfer({ status: 'fehler', repo: 'a/b', failed: 2 }).title, 'Upload fehlgeschlagen');
assert.match(describeTransfer({ status: 'fehler', repo: 'a/b', failed: 2 }).body, /2 Dateien fehlgeschlagen/);
assert.match(describeTransfer({ status: 'fehler', repo: 'a/b', error: 'Server nicht erreichbar' }).body, /Server nicht erreichbar/);
assert.match(describeTransfer({ status: 'fehler', repo: '' }).body, /Unbekannter Fehler/);
});
test('Langer Fehlertext wird gekürzt', () => {
assert.ok(describeTransfer({ status: 'fehler', repo: 'a/b', error: 'x'.repeat(500) }).body.length < 200);
});
+133
View File
@@ -0,0 +1,133 @@
const test = require('node:test');
const assert = require('node:assert');
const { buildTrayTemplate, trayTooltip } = require('../src/features/trayMenu.js');
const noop = () => {};
const base = { version: '2.2.1', onOpen: noop, onHistory: noop, onCancel: noop, onAutostart: noop, onQuit: noop };
const labels = (tpl) => tpl.filter(i => i.label).map(i => i.label);
const find = (tpl, label) => tpl.find(i => i.label === label);
test('Tooltip nennt Version und zeigt eine laufende Übertragung', () => {
assert.strictEqual(trayTooltip('2.2.1', 0), 'Git Manager Explorer Pro v2.2.1');
assert.match(trayTooltip('2.2.1', 2), /Übertragung läuft/);
assert.match(trayTooltip('2.2.1', 1, true), /Übertragung pausiert/);
assert.strictEqual(trayTooltip('2.2.1', 0, true), 'Git Manager Explorer Pro v2.2.1');
});
test('Menü im Ruhezustand: Kopfzeile, Status, Aktionen; Abbrechen gesperrt', () => {
const tpl = buildTrayTemplate(base);
assert.deepStrictEqual(labels(tpl), [
'Git Manager Explorer Pro v2.2.1',
'Keine laufende Übertragung',
'Fenster öffnen',
'Upload-Verlauf',
'Übertragung pausieren',
'Laufende Übertragung abbrechen',
'Mit Windows starten',
'Beenden'
]);
assert.strictEqual(find(tpl, 'Laufende Übertragung abbrechen').enabled, false);
assert.strictEqual(tpl[0].enabled, false);
});
test('Menü bei laufender Übertragung: Zähler, Abbrechen freigeschaltet', () => {
const one = buildTrayTemplate({ ...base, active: 1 });
assert.ok(labels(one).includes('1 Übertragung läuft'));
assert.strictEqual(find(one, 'Laufende Übertragung abbrechen').enabled, true);
assert.ok(labels(buildTrayTemplate({ ...base, active: 3 })).includes('3 Übertragungen laufen'));
});
test('Retry-Queue erscheint nur, wenn etwas wartet', () => {
assert.ok(!labels(buildTrayTemplate(base)).some(l => l.startsWith('Retry-Queue')));
assert.ok(labels(buildTrayTemplate({ ...base, queued: 4 })).includes('Retry-Queue: 4 wartend'));
});
test('Autostart ist ein Häkchen und spiegelt den Zustand', () => {
assert.strictEqual(find(buildTrayTemplate({ ...base, autostart: true }), 'Mit Windows starten').checked, true);
const off = find(buildTrayTemplate(base), 'Mit Windows starten');
assert.strictEqual(off.type, 'checkbox');
assert.strictEqual(off.checked, false);
});
test('Einträge rufen die übergebenen Aktionen auf', () => {
const calls = [];
const tpl = buildTrayTemplate({
...base,
active: 1,
onOpen: () => calls.push('open'),
onHistory: () => calls.push('history'),
onCancel: () => calls.push('cancel'),
onQuit: () => calls.push('quit')
});
for (const l of ['Fenster öffnen', 'Upload-Verlauf', 'Laufende Übertragung abbrechen', 'Beenden']) find(tpl, l).click();
assert.deepStrictEqual(calls, ['open', 'history', 'cancel', 'quit']);
});
const { serializeTemplate, runAction } = require('../src/features/trayMenu.js');
const { computePosition } = require('../src/features/trayPopup.js');
test('serializeTemplate: nur Daten, keine Funktionen, Trenner bleiben erhalten', () => {
const plain = serializeTemplate(buildTrayTemplate({ ...base, active: 1, autostart: true }));
assert.ok(plain.every(i => Object.values(i).every(v => typeof v !== 'function')));
assert.strictEqual(plain.filter(i => i.type === 'separator').length, 3);
const cancel = plain.find(i => i.id === 'cancel');
assert.deepStrictEqual(cancel, { id: 'cancel', label: 'Laufende Übertragung abbrechen', type: 'normal', enabled: true, checked: false });
assert.strictEqual(plain.find(i => i.id === 'autostart').checked, true);
assert.doesNotThrow(() => JSON.stringify(plain));
});
test('runAction: führt Aktionen per id aus, Häkchen wird umgeschaltet', () => {
const calls = [];
const tpl = buildTrayTemplate({
...base,
active: 1,
autostart: false,
onOpen: () => calls.push('open'),
onAutostart: (item) => calls.push(`autostart:${item.checked}`)
});
assert.strictEqual(runAction(tpl, 'open'), true);
assert.strictEqual(runAction(tpl, 'autostart'), true);
assert.deepStrictEqual(calls, ['open', 'autostart:true']);
});
test('runAction: gesperrte oder unbekannte Einträge tun nichts', () => {
let cancelled = false;
const tpl = buildTrayTemplate({ ...base, active: 0, onCancel: () => { cancelled = true; } });
assert.strictEqual(runAction(tpl, 'cancel'), false);
assert.strictEqual(runAction(tpl, 'gibt-es-nicht'), false);
assert.strictEqual(runAction(tpl, 'header'), false);
assert.strictEqual(cancelled, false);
});
test('computePosition: Menü bleibt im sichtbaren Bereich', () => {
const wa = { x: 0, y: 0, width: 1920, height: 1040 };
// Zeiger in der Taskleiste unten rechts: Menü sitzt direkt über der Taskleiste, nicht außerhalb rechts
const bottomRight = computePosition({ x: 1900, y: 1060 }, wa, 286, 300);
assert.strictEqual(bottomRight.y, 1040 - 300 - 8);
assert.ok(bottomRight.x + 286 <= 1920 - 8);
// Zeiger weit links: nicht links aus dem Bildschirm
assert.ok(computePosition({ x: 2, y: 1060 }, wa, 286, 300).x >= 8);
// Taskleiste oben
assert.strictEqual(computePosition({ x: 500, y: 10 }, { ...wa, y: 40, height: 1000 }, 286, 300).y, 48);
});
test('Pause: Eintrag wechselt zwischen pausieren und fortsetzen, nur bei laufender Übertragung wählbar', () => {
const idle = buildTrayTemplate(base);
assert.strictEqual(find(idle, 'Übertragung pausieren').enabled, false);
const running = buildTrayTemplate({ ...base, active: 1 });
assert.strictEqual(find(running, 'Übertragung pausieren').enabled, true);
const paused = buildTrayTemplate({ ...base, active: 2, paused: true });
assert.ok(labels(paused).includes('Übertragung fortsetzen'));
assert.ok(labels(paused).includes('2 Übertragungen pausiert'));
assert.ok(!labels(paused).includes('Übertragung pausieren'));
assert.ok(labels(buildTrayTemplate({ ...base, active: 1, paused: true })).includes('1 Übertragung pausiert'));
});
test('Pause-Eintrag ruft onPause auf', () => {
const calls = [];
const tpl = buildTrayTemplate({ ...base, active: 1, onPause: () => calls.push('pause') });
assert.strictEqual(runAction(tpl, 'pause'), true);
assert.deepStrictEqual(calls, ['pause']);
});
+35
View File
@@ -0,0 +1,35 @@
const test = require('node:test');
const assert = require('node:assert');
const { blockedReason, partitionBlocked } = require('../src/features/uploadBlocklist.js');
test('Sperre: Dateien mit Zugangsdaten werden erkannt', () => {
for (const name of ['credentials.json', 'data/credentials.json', 'C:\\proj\\.env', '.env.production', 'id_rsa', 'release.keystore', 'server.pem', 'secrets.json', '.npmrc']) {
assert.ok(blockedReason(name, {}), `${name} sollte gesperrt sein`);
}
});
test('Sperre: normale Dateien und Vorlagen bleiben erlaubt', () => {
for (const name of ['main.js', 'package.json', 'README.md', '.env.example', 'config.sample.json', 'id_rsa.pub', 'credentials.md', 'src/credentials/index.js']) {
assert.strictEqual(blockedReason(name, {}), null, `${name} darf hochgeladen werden`);
}
});
test('Sperre: Einstellung kann sie abschalten, Standard ist an', () => {
assert.ok(blockedReason('credentials.json', undefined));
assert.ok(blockedReason('credentials.json', { uploadBlockSecrets: true }));
assert.strictEqual(blockedReason('credentials.json', { uploadBlockSecrets: false }), null);
});
test('partitionBlocked: trennt Einträge und behält die Reihenfolge', () => {
const items = [
{ localFile: 'a/main.js', targetPath: 'a/main.js' },
{ localFile: 'a/data/credentials.json', targetPath: 'a/data/credentials.json' },
{ localFile: 'a/.env', targetPath: 'a/.env' },
{ localFile: 'a/README.md', targetPath: 'a/README.md' }
];
const { allowed, blocked } = partitionBlocked(items, {});
assert.deepStrictEqual(allowed.map(i => i.localFile), ['a/main.js', 'a/README.md']);
assert.deepStrictEqual(blocked.map(b => b.path), ['a/data/credentials.json', 'a/.env']);
assert.ok(blocked.every(b => b.reason));
assert.strictEqual(partitionBlocked(items, { uploadBlockSecrets: false }).blocked.length, 0);
});
+146
View File
@@ -0,0 +1,146 @@
const test = require('node:test');
const assert = require('node:assert');
const http = require('node:http');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const {
uploadItemsViaGiteaApi,
createUploadCancelledError,
isUploadCancelledError,
throwIfUploadCancelled
} = require('../src/git/apiHandler.js');
test('Abbruchfehler: Erkennung und throwIfUploadCancelled', () => {
assert.strictEqual(isUploadCancelledError(createUploadCancelledError()), true);
assert.strictEqual(isUploadCancelledError(new Error('anderer Fehler')), false);
assert.strictEqual(isUploadCancelledError(null), false);
assert.doesNotThrow(() => throwIfUploadCancelled(undefined));
assert.doesNotThrow(() => throwIfUploadCancelled(new AbortController().signal));
const c = new AbortController();
c.abort();
assert.throws(() => throwIfUploadCancelled(c.signal), (e) => isUploadCancelledError(e));
});
function makeTempFile() {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gm-cancel-'));
const file = path.join(dir, 'a.txt');
fs.writeFileSync(file, 'hallo');
return { dir, file };
}
test('API-Upload: bereits abgebrochenes Signal sendet nichts', async () => {
const { dir, file } = makeTempFile();
try {
const c = new AbortController();
c.abort();
await assert.rejects(
uploadItemsViaGiteaApi({
token: 't', url: 'http://127.0.0.1:1', owner: 'o', repo: 'r', branch: 'main',
items: [{ localFile: file, targetPath: 'a.txt' }], message: 'm', signal: c.signal
}),
(e) => isUploadCancelledError(e)
);
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
});
test('API-Upload: Abbruch während der Übertragung wirft Abbruchfehler und landet nicht in "failed"', async () => {
const { dir, file } = makeTempFile();
let postSeen = false;
const server = http.createServer((req, res) => {
if (req.method === 'POST') {
postSeen = true;
setTimeout(() => { res.writeHead(201, { 'content-type': 'application/json' }); res.end('{}'); }, 3000);
return;
}
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ default_branch: 'main', tree: [], entries: [] }));
});
await new Promise(r => server.listen(0, '127.0.0.1', r));
const { port } = server.address();
try {
const c = new AbortController();
const started = Date.now();
setTimeout(() => c.abort(), 400);
await assert.rejects(
uploadItemsViaGiteaApi({
token: 't', url: `http://127.0.0.1:${port}`, owner: 'o', repo: 'r', branch: 'main',
items: [{ localFile: file, targetPath: 'a.txt' }], message: 'm', signal: c.signal
}),
(e) => isUploadCancelledError(e)
);
assert.ok(postSeen, 'Der POST muss gestartet sein, sonst prüft der Test nichts');
assert.ok(Date.now() - started < 2500, 'Abbruch muss sofort greifen, nicht erst nach der Serverantwort');
} finally {
server.closeAllConnections?.();
await new Promise(r => server.close(r));
fs.rmSync(dir, { recursive: true, force: true });
}
});
const { createPauseGate } = require('../src/features/pauseGate.js');
function gitea(onPost) {
return http.createServer((req, res) => {
if (req.method === 'POST') {
onPost();
res.writeHead(201, { 'content-type': 'application/json' });
res.end('{}');
return;
}
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ default_branch: 'main', tree: [], entries: [] }));
});
}
test('API-Upload: während der Pause wird nichts gesendet, nach resume() schon', async () => {
const { dir, file } = makeTempFile();
let posts = 0;
const server = gitea(() => { posts++; });
await new Promise(r => server.listen(0, '127.0.0.1', r));
const { port } = server.address();
const gate = createPauseGate();
try {
gate.pause();
const upload = uploadItemsViaGiteaApi({
token: 't', url: `http://127.0.0.1:${port}`, owner: 'o', repo: 'r', branch: 'main',
items: [{ localFile: file, targetPath: 'a.txt' }], message: 'm',
waitIfPaused: (s) => gate.wait(s)
});
await new Promise(r => setTimeout(r, 700));
assert.strictEqual(posts, 0, 'Während der Pause darf kein Paket gesendet werden');
gate.resume();
const result = await upload;
assert.strictEqual(posts, 1);
assert.deepStrictEqual(result.uploaded, ['a.txt']);
} finally {
await new Promise(r => server.close(r));
fs.rmSync(dir, { recursive: true, force: true });
}
});
test('API-Upload: Abbruch während der Pause beendet den Upload sofort', async () => {
const { dir, file } = makeTempFile();
let posts = 0;
const server = gitea(() => { posts++; });
await new Promise(r => server.listen(0, '127.0.0.1', r));
const { port } = server.address();
const gate = createPauseGate();
try {
gate.pause();
const c = new AbortController();
const upload = uploadItemsViaGiteaApi({
token: 't', url: `http://127.0.0.1:${port}`, owner: 'o', repo: 'r', branch: 'main',
items: [{ localFile: file, targetPath: 'a.txt' }], message: 'm',
signal: c.signal, waitIfPaused: (s) => gate.wait(s)
});
setTimeout(() => c.abort(), 400);
await assert.rejects(upload, (e) => isUploadCancelledError(e));
assert.strictEqual(posts, 0);
assert.strictEqual(gate.waitingCount(), 0);
} finally {
await new Promise(r => server.close(r));
fs.rmSync(dir, { recursive: true, force: true });
}
});
+113
View File
@@ -0,0 +1,113 @@
const test = require('node:test');
const assert = require('node:assert');
const { buildEntry, filterEntries, createUploadHistory } = require('../src/features/uploadHistory.js');
function memoryFs(initial) {
const files = new Map(initial ? [['h.json', initial]] : []);
return {
files,
readFileSync: (f) => { if (!files.has(f)) throw new Error('ENOENT'); return files.get(f); },
writeFileSync: (f, c) => { files.set(f, c); }
};
}
test('buildEntry: erfolgreicher Upload', () => {
const e = buildEntry('Drag & Drop', { owner: 'a', repo: 'b' }, { ok: true, results: [{}, {}, {}] }, 1000, null, 4000);
assert.strictEqual(e.status, 'ok');
assert.strictEqual(e.repo, 'a/b');
assert.strictEqual(e.files, 3);
assert.strictEqual(e.durationMs, 3000);
assert.strictEqual(e.error, '');
});
test('buildEntry: abgebrochen ist kein Fehler und trägt keinen Fehlertext', () => {
const e = buildEntry('Ordner', { owner: 'a', repo: 'b' }, { ok: false, cancelled: true, error: 'Upload abgebrochen' }, 0, null, 10);
assert.strictEqual(e.status, 'abgebrochen');
assert.strictEqual(e.error, '');
});
test('buildEntry: Fehler und geworfene Ausnahmen', () => {
const failed = buildEntry('Dateien', { owner: 'a', repo: 'b' }, { ok: false, error: 'kaputt', failedCount: 2 }, 0, null, 10);
assert.strictEqual(failed.status, 'fehler');
assert.strictEqual(failed.error, 'kaputt');
assert.strictEqual(failed.failed, 2);
const thrown = buildEntry('Dateien', {}, null, 0, new Error('boom'), 10);
assert.strictEqual(thrown.status, 'fehler');
assert.strictEqual(thrown.error, 'boom');
assert.strictEqual(thrown.repo, '');
});
test('buildEntry: Einzeldatei zählt als eine Datei, lange Fehler werden gekürzt', () => {
assert.strictEqual(buildEntry('x', {}, { ok: true, singleFile: true }, 0, null, 1).files, 1);
assert.strictEqual(buildEntry('x', {}, { ok: false, error: 'e'.repeat(1000) }, 0, null, 1).error.length, 300);
});
test('filterEntries: nach Status und Repo-Suche', () => {
const list = [
{ repo: 'a/alpha', status: 'ok' },
{ repo: 'a/beta', status: 'fehler' },
{ repo: 'b/Alpha-2', status: 'abgebrochen' }
];
assert.strictEqual(filterEntries(list).length, 3);
assert.deepStrictEqual(filterEntries(list, { status: 'fehler' }).map(e => e.repo), ['a/beta']);
assert.deepStrictEqual(filterEntries(list, { query: 'ALPHA' }).map(e => e.repo), ['a/alpha', 'b/Alpha-2']);
assert.strictEqual(filterEntries(list, { status: 'ok', query: 'beta' }).length, 0);
});
test('Verlauf: neueste zuerst, auf max Einträge begrenzt', () => {
const history = createUploadHistory({ file: 'h.json', max: 3, fs: memoryFs() });
for (let i = 1; i <= 5; i++) history.record({ n: i });
assert.deepStrictEqual(history.read().map(e => e.n), [5, 4, 3]);
});
test('Verlauf: kaputte oder fehlende Datei ergibt eine leere Liste', () => {
assert.deepStrictEqual(createUploadHistory({ file: 'h.json', fs: memoryFs() }).read(), []);
assert.deepStrictEqual(createUploadHistory({ file: 'h.json', fs: memoryFs('{kaputt') }).read(), []);
assert.deepStrictEqual(createUploadHistory({ file: 'h.json', fs: memoryFs('{"a":1}') }).read(), []);
});
test('Verlauf: leeren', () => {
const history = createUploadHistory({ file: 'h.json', fs: memoryFs() });
history.record({ n: 1 });
history.clear();
assert.deepStrictEqual(history.read(), []);
});
test('Verlauf: Schreibfehler werfen nicht', () => {
const broken = { readFileSync: () => '[]', writeFileSync: () => { throw new Error('Platte voll'); } };
assert.strictEqual(createUploadHistory({ file: 'h.json', fs: broken }).record({ n: 1 }), false);
});
const { toCsv, formatDate } = require('../src/features/uploadHistory.js');
test('formatDate: TT.MM.JJJJ HH:MM:SS, ungültige Eingabe ergibt leeren Text', () => {
assert.match(formatDate('2026-10-22T07:11:53.080Z'), /^\d{2}\.\d{2}\.2026 \d{2}:\d{2}:\d{2}$/);
assert.strictEqual(formatDate('kein Datum'), '');
});
test('toCsv: BOM, Semikolon, Windows-Zeilenenden, Kopfzeile', () => {
const csv = toCsv([{ at: '2026-10-22T07:11:53.080Z', repo: 'a/b', kind: 'Drag & Drop', files: 3, failed: 0, status: 'ok', durationMs: 2500, error: '' }]);
assert.strictEqual(csv.charCodeAt(0), 0xFEFF);
const lines = csv.slice(1).split('\r\n');
assert.strictEqual(lines[0], 'Datum;Repo;Art;Dateien;Fehlgeschlagen;Ergebnis;Dauer (s);Fehler');
assert.match(lines[1], /;a\/b;Drag & Drop;3;0;Erfolgreich;2\.5;$/);
assert.strictEqual(lines[2], '');
});
test('toCsv: Anführungszeichen, Semikolon und Zeilenumbrüche werden maskiert', () => {
const csv = toCsv([{ at: '2026-10-22T07:11:53Z', repo: 'a/b', kind: 'x', files: null, failed: 1, status: 'fehler', durationMs: 100, error: 'sagt "nein"; und\nmehr' }]);
assert.ok(csv.includes('"sagt ""nein""; und\nmehr"'));
assert.match(csv, /;Fehler;0\.1;/);
});
test('toCsv: Excel-Formeln werden entschärft', () => {
const csv = toCsv([{ at: '2026-10-22T07:11:53Z', repo: '=cmd|calc', kind: '+x', files: 1, failed: 0, status: 'ok', durationMs: 0, error: '@SUMME(A1)' }]);
assert.ok(csv.includes(";'=cmd|calc;"));
assert.ok(csv.includes(";'+x;"));
assert.ok(csv.includes(";'@SUMME(A1)"));
});
test('toCsv: leere Liste liefert nur die Kopfzeile', () => {
assert.strictEqual(toCsv([]).slice(1).split('\r\n').filter(Boolean).length, 1);
});
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
View File
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

+19
View File
@@ -0,0 +1,19 @@
<Application x:Class="GitManagerSetup.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- [0] wird je nach Hell/Dunkel ersetzt -->
<ResourceDictionary Source="Themes/Colors.Light.xaml"/>
<ResourceDictionary Source="Themes/Controls.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- Akzent wie in der App (Cyan → Blau) -->
<SolidColorBrush x:Key="AccentBrush" Color="#2E7FE0"/>
<SolidColorBrush x:Key="AccentHoverBrush" Color="#4590EA"/>
<SolidColorBrush x:Key="OnAccentBrush" Color="White"/>
<SolidColorBrush x:Key="AccentSubtleBrush" Color="#222E7FE0"/>
<SolidColorBrush x:Key="AccentSubtleStrokeBrush" Color="#552E7FE0"/>
</ResourceDictionary>
</Application.Resources>
</Application>
+99
View File
@@ -0,0 +1,99 @@
using System.IO;
using System.Windows;
using System.Windows.Media;
using Microsoft.Win32;
namespace GitManagerSetup;
public partial class App : Application
{
private static Mutex? _single;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
bool has(string arg) => e.Args.Any(a => a.Equals(arg, StringComparison.OrdinalIgnoreCase));
// Das Deinstallationsprogramm ist das Setup ohne angehängte App → ohne Anhang immer Deinstallation
bool uninstall = has("--uninstall") || !Payload.Exists;
bool silent = has("/S");
bool update = has("--update");
bool launch = has("/launch");
var dirArg = e.Args.FirstOrDefault(a => a.StartsWith("/D=", StringComparison.OrdinalIgnoreCase))?.Substring(3);
_single = new Mutex(true, "GitManagerGUI-Setup", out bool first);
if (!first)
{
Shutdown(0);
return;
}
// Stille Installation/Deinstallation für Skripte:
// Git-Manager-GUI-Setup.exe /S [/D=Ordner] [/launch] bzw. --uninstall /S
if (silent)
{
var progress = new Progress<(double, string)>(_ => { });
try
{
if (uninstall) Installer.Uninstall(removeData: false, progress);
else
{
var dir = dirArg ?? Installer.InstalledDir ?? Installer.DefaultDir;
Installer.Install(dir, desktopLink: Installer.InstalledDir is null || Installer.DesktopLinkExists,
progress, waitForExit: true);
if (launch) System.Diagnostics.Process.Start(Path.Combine(dir, Installer.ExeName));
}
Shutdown(0);
}
catch (Exception ex)
{
// ohne Oberfläche: Fehler für Skripte nachvollziehbar ablegen
try { File.WriteAllText(Path.Combine(Path.GetTempPath(), "Git-Manager-GUI-Setup.log"), $"[{DateTime.Now}] {ex}"); } catch { }
Shutdown(1);
}
return;
}
ApplyTheme();
var mode = uninstall ? SetupMode.Uninstall : update ? SetupMode.Update : SetupMode.Install;
new MainWindow(mode).Show();
}
protected override void OnExit(ExitEventArgs e)
{
try { _single?.ReleaseMutex(); } catch { }
base.OnExit(e);
}
/// <summary>Hell/Dunkel wie in Windows eingestellt.</summary>
private void ApplyTheme()
{
bool light = ReadDword(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme") != 0;
Resources.MergedDictionaries[0] = new ResourceDictionary
{
Source = new Uri($"pack://application:,,,/Themes/Colors.{(light ? "Light" : "Dark")}.xaml"),
};
if (!light)
{
// im Dunkeln das Cyan der App, damit Text und Fortschrittsbalken gut lesbar bleiben
var a = Color.FromRgb(0x4C, 0xB8, 0xF5);
Resources["AccentBrush"] = new SolidColorBrush(a);
Resources["AccentHoverBrush"] = new SolidColorBrush(Color.FromRgb(0x67, 0xC6, 0xF8));
Resources["OnAccentBrush"] = new SolidColorBrush(Color.FromRgb(0x04, 0x10, 0x1F));
Resources["AccentSubtleBrush"] = new SolidColorBrush(Color.FromArgb(0x30, a.R, a.G, a.B));
Resources["AccentSubtleStrokeBrush"] = new SolidColorBrush(Color.FromArgb(0x60, a.R, a.G, a.B));
}
}
private static int ReadDword(string key, string name)
{
try
{
using var k = Registry.CurrentUser.OpenSubKey(key);
return k?.GetValue(name) is int i ? i : -1;
}
catch { return -1; }
}
}
public enum SetupMode { Install, Update, Uninstall }
+46
View File
@@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<!-- .NET Framework 4.8 ist in jedem Windows 10/11 enthalten → der Installer läuft ohne Zusatzinstallation -->
<TargetFramework>net48</TargetFramework>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PlatformTarget>AnyCPU</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<AssemblyName>Git-Manager-GUI-Setup</AssemblyName>
<RootNamespace>GitManagerSetup</RootNamespace>
<ApplicationIcon>..\assets\icon.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Product>Git Manager GUI Setup</Product>
<AssemblyTitle>Git Manager GUI Setup</AssemblyTitle>
<Description>Installiert Git Manager GUI</Description>
<Company>M_Viper</Company>
<Authors>M_Viper</Authors>
<Copyright>Copyright (c) M_Viper</Copyright>
<NeutralLanguage>de-DE</NeutralLanguage>
<!-- Version kommt beim Bauen aus package.json (scripts/build-setup.js) -->
<Version Condition="'$(Version)' == ''">0.0.0</Version>
</PropertyGroup>
<ItemGroup>
<!-- WinForms nur für den Ordner-Dialog; nicht global, sonst Namenskonflikte mit WPF -->
<Using Remove="System.Windows.Forms" />
<Using Remove="System.Drawing" />
<Reference Include="Microsoft.VisualBasic" />
<Reference Include="System.IO.Compression" />
</ItemGroup>
<ItemGroup>
<!-- Themes\*.xaml bindet das SDK automatisch als Page ein -->
<Resource Include="..\assets\icon.ico" Link="app.ico" />
<Resource Include="..\assets\icon.png" Link="app.png" />
</ItemGroup>
<!--
Die eigentliche App (entpackter Electron-Ordner als ZIP) wird NICHT hier eingebettet,
sondern nach dem Bauen hinten an die .exe angehängt (scripts/build-setup.js, siehe Payload.cs).
So muss der Compiler keine ~150 MB als Ressource verarbeiten, und das Deinstallationsprogramm
ist einfach der vordere Teil der Datei ohne Anhang.
-->
</Project>
+501
View File
@@ -0,0 +1,501 @@
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Reflection;
using System.Text.RegularExpressions;
using Microsoft.Win32;
namespace GitManagerSetup;
/// <summary>Installation/Deinstallation – nur für den aktuellen Benutzer, ohne Administratorrechte.</summary>
public static class Installer
{
public const string AppName = "Git Manager GUI";
public const string Publisher = "M_Viper";
// productName aus package.json → so heißt die EXE im entpackten Electron-Ordner
public const string ExeName = "Git Manager GUI.exe";
private const string ProcessName = "Git Manager GUI";
// Gleicher Name wie beim früheren NSIS-Setup (electron-builder), damit eine alte Installation sauber ersetzt wird
public const string UninstallerName = "Uninstall Git Manager GUI.exe";
// wie "appId" in package.json – gleiche Taskleisten-/Benachrichtigungs-Zuordnung wie bisher
public const string AppUserModelId = "com.viper.gitmanager";
// Schlüssel vom alten electron-builder-Setup (Version 2.1.7) übernommen → in "Apps" bleibt es ein einziger Eintrag
private const string KeyId = "4156cc7f-a3e7-5b1d-9238-0af3e0f54ecd";
private const string UninstallKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall\" + KeyId;
private const string LegacyAppKey = @"Software\" + KeyId;
private const string RunKey = @"Software\Microsoft\Windows\CurrentVersion\Run";
private const string FileListName = "install-files.txt";
public static string Version
{
get
{
var info = typeof(Installer).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
if (!string.IsNullOrEmpty(info)) return info!.Split('+')[0];
var v = typeof(Installer).Assembly.GetName().Version!;
return $"{v.Major}.{v.Minor}.{v.Build}";
}
}
/// <summary>Pfad dieses Setups (bzw. des Deinstallationsprogramms).</summary>
public static string SelfPath => Assembly.GetExecutingAssembly().Location;
public static string DefaultDir =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", AppName);
/// <summary>App-Daten: Zugangsdaten (verschlüsselt), Einstellungen, Favoriten, Verlauf, Backup-Einstellung.</summary>
public static string DataDir =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "git-manager-gui");
private static string StartMenuLink =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Programs), AppName + ".lnk");
private static string DesktopLink =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), AppName + ".lnk");
// ---------- vorhandene Installation ----------
private static string? RegValue(string name)
{
using var k = Registry.CurrentUser.OpenSubKey(UninstallKey);
return k?.GetValue(name) as string;
}
/// <summary>Ordner einer vorhandenen Installation (dieses Setups oder des alten NSIS-Setups), oder null.</summary>
public static string? InstalledDir
{
get
{
var dir = RegValue("InstallLocation");
if (string.IsNullOrWhiteSpace(dir))
{
// altes NSIS-Setup trägt keinen InstallLocation ein → aus dem Deinstallationsbefehl ablesen
var m = Regex.Match(RegValue("UninstallString") ?? "", "^\"([^\"]+)\"");
dir = m.Success ? Path.GetDirectoryName(m.Groups[1].Value) : null;
}
return dir is not null && File.Exists(Path.Combine(dir, ExeName)) ? dir : null;
}
}
/// <summary>Stammt die vorhandene Installation noch vom alten NSIS-Setup?</summary>
public static bool IsLegacyInstall =>
InstalledDir is not null && (RegValue("UninstallString") ?? "").IndexOf("--uninstall", StringComparison.OrdinalIgnoreCase) < 0;
public static string? InstalledVersion => RegValue("DisplayVersion");
public static bool DesktopLinkExists => File.Exists(DesktopLink);
public static bool IsRunning(string? dir) => AppProcesses(dir, InstalledDir).Any();
// ---------- Installieren ----------
/// <param name="waitForExit">nach einem Update aus der App: ihr erst Zeit geben, sich selbst zu beenden</param>
public static void Install(string dir, bool desktopLink, IProgress<(double, string)> progress, bool waitForExit = false)
{
if (!Environment.Is64BitOperatingSystem)
throw new InvalidOperationException("Git Manager GUI benötigt ein 64-Bit-Windows (Windows 10 oder 11).");
var oldDir = InstalledDir;
progress.Report((0.02, "Git Manager GUI wird beendet …"));
CloseRunningApp(waitForExit ? 20000 : 0, dir, oldDir);
using var zip = Payload.OpenArchive();
var topLevel = TopLevelNames(zip);
progress.Report((0.05, "Alte Version wird entfernt …"));
Directory.CreateDirectory(dir);
RemoveAppFiles(dir, topLevel);
if (oldDir is not null && !SamePath(oldDir, dir))
{
// Installation wurde in einen anderen Ordner verlegt → alten Ordner aufräumen
var oldExe = Path.Combine(oldDir, ExeName);
DeleteIfPointsTo(StartMenuLink, oldExe);
DeleteIfPointsTo(DesktopLink, oldExe);
RemoveAppFiles(oldDir, topLevel);
try { Directory.Delete(oldDir); } catch { } // nur wenn leer
}
Extract(zip, dir, p => progress.Report((0.08 + p * 0.8, "Dateien werden kopiert …")));
progress.Report((0.9, "Deinstallationsprogramm wird angelegt …"));
var exe = Path.Combine(dir, ExeName);
var uninstaller = Path.Combine(dir, UninstallerName);
if (!SamePath(SelfPath, uninstaller))
{
MoveAside(uninstaller);
CopyStub(uninstaller);
}
File.WriteAllLines(Path.Combine(dir, FileListName), topLevel.Concat([UninstallerName, FileListName]));
progress.Report((0.94, "Verknüpfungen werden angelegt …"));
Shortcut.Create(StartMenuLink, exe, "Repositories auf Gitea und GitHub verwalten", AppUserModelId);
if (desktopLink) Shortcut.Create(DesktopLink, exe, "Repositories auf Gitea und GitHub verwalten", AppUserModelId);
else DeleteIfPointsTo(DesktopLink, exe);
progress.Report((0.97, "Windows wird eingerichtet …"));
Registry.CurrentUser.DeleteSubKeyTree(UninstallKey, throwOnMissingSubKey: false); // Reste des alten Setups
Registry.CurrentUser.DeleteSubKeyTree(LegacyAppKey, throwOnMissingSubKey: false);
using (var k = Registry.CurrentUser.CreateSubKey(UninstallKey))
{
long size = DirSize(dir) / 1024;
k.SetValue("DisplayName", AppName);
k.SetValue("DisplayVersion", Version);
k.SetValue("Publisher", Publisher);
k.SetValue("DisplayIcon", $"{exe},0");
k.SetValue("InstallLocation", dir);
k.SetValue("UninstallString", $"\"{uninstaller}\" --uninstall");
k.SetValue("QuietUninstallString", $"\"{uninstaller}\" --uninstall /S");
k.SetValue("URLInfoAbout", "https://git.viper.ipv64.net/M_Viper/Git-Manager-Gui");
k.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd"));
k.SetValue("EstimatedSize", (int)Math.Min(int.MaxValue, size), RegistryValueKind.DWord);
k.SetValue("NoModify", 1, RegistryValueKind.DWord);
k.SetValue("NoRepair", 1, RegistryValueKind.DWord);
}
progress.Report((0.98, "Installation wird abgeschlossen …"));
Vorwaermen(exe);
progress.Report((1.0, "Fertig"));
}
/// <summary>
/// Startet die App einmal kurz mit "--gm-vorwaermen" – sie beendet sich
/// damit sofort wieder (siehe main.js). Windows Defender prüft eine neue,
/// noch unbekannte EXE beim ersten Start mehrere Sekunden lang (gemessen: ~8 s);
/// das passiert so während des Fortschrittsbalkens und nicht erst beim Klick
/// auf "Fertigstellen". Das Ergebnis merkt sich Windows, der echte Start ist
/// danach sofort da.
/// </summary>
private static void Vorwaermen(string exe)
{
try
{
using var p = Process.Start(new ProcessStartInfo(exe, "--gm-vorwaermen")
{
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = Path.GetDirectoryName(exe)!,
});
if (p != null && !p.WaitForExit(30000)) { try { p.Kill(); } catch { } }
}
catch { /* nur eine Beschleunigung – Fehler sind egal */ }
}
private static List<string> TopLevelNames(ZipArchive zip) =>
zip.Entries
.Select(e => e.FullName.Replace('\\', '/').Split('/')[0])
.Where(n => n.Length > 0)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
private static void Extract(ZipArchive zip, string dir, Action<double> report)
{
var root = Path.GetFullPath(dir).TrimEnd('\\') + "\\";
long total = Math.Max(1, zip.Entries.Sum(e => e.Length)), done = 0;
var buffer = new byte[1 << 20];
int lastPercent = -1;
foreach (var entry in zip.Entries)
{
var target = Path.GetFullPath(Path.Combine(dir, entry.FullName));
if (!target.StartsWith(root, StringComparison.OrdinalIgnoreCase)) continue; // nie außerhalb des Ordners schreiben
if (entry.Name.Length == 0)
{
Directory.CreateDirectory(target);
continue;
}
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
long before = done;
Retry(target, () =>
{
done = before;
MoveAside(target);
using var src = entry.Open();
using var dst = File.Create(target);
int n;
while ((n = src.Read(buffer, 0, buffer.Length)) > 0)
{
dst.Write(buffer, 0, n);
done += n;
int percent = (int)(done * 100 / total);
if (percent != lastPercent)
{
lastPercent = percent;
report(Math.Min(1.0, (double)done / total));
}
}
});
}
report(1.0);
}
/// <summary>Setup ohne angehängte App als Deinstallationsprogramm ablegen (nur ein paar hundert KB).</summary>
private static void CopyStub(string path)
{
long length = Payload.StubLength;
using var src = File.OpenRead(SelfPath);
using var dst = File.Create(path);
var buffer = new byte[81920];
long left = length;
while (left > 0)
{
int n = src.Read(buffer, 0, (int)Math.Min(buffer.Length, left));
if (n <= 0) break;
dst.Write(buffer, 0, n);
left -= n;
}
}
// ---------- Deinstallieren ----------
public static void Uninstall(bool removeData, IProgress<(double, string)> progress)
{
// Nur im eingetragenen Installationsordner löschen – nie dort, wo das Setup zufällig liegt (z. B. Downloads)
var dir = InstalledDir;
var exe = dir is null ? "" : Path.Combine(dir, ExeName);
progress.Report((0.1, "Git Manager GUI wird beendet …"));
CloseRunningApp(0, dir);
progress.Report((0.3, "Verknüpfungen werden entfernt …"));
DeleteIfPointsTo(StartMenuLink, exe);
DeleteIfPointsTo(DesktopLink, exe);
progress.Report((0.45, "Windows-Einträge werden entfernt …"));
RemoveAutostart(exe);
Registry.CurrentUser.DeleteSubKeyTree(UninstallKey, throwOnMissingSubKey: false);
Registry.CurrentUser.DeleteSubKeyTree(LegacyAppKey, throwOnMissingSubKey: false);
// Name/Logo für Windows-Benachrichtigungen (legt Electron selbst an)
Registry.CurrentUser.DeleteSubKeyTree(@"Software\Classes\AppUserModelId\" + AppUserModelId, throwOnMissingSubKey: false);
if (removeData && Directory.Exists(DataDir))
{
progress.Report((0.6, "Zugangsdaten und Einstellungen werden in den Papierkorb gelegt …"));
try
{
Microsoft.VisualBasic.FileIO.FileSystem.DeleteDirectory(DataDir,
Microsoft.VisualBasic.FileIO.UIOption.OnlyErrorDialogs,
Microsoft.VisualBasic.FileIO.RecycleOption.SendToRecycleBin);
}
catch { }
}
progress.Report((0.8, "Dateien werden entfernt …"));
if (dir is null) { progress.Report((1.0, "Fertig")); return; }
RemoveAppFiles(dir, null);
// Das laufende Deinstallationsprogramm kann sich nicht selbst löschen → kurz nach dem Beenden per cmd.
// rmdir ohne /s entfernt den Ordner nur, wenn er danach leer ist.
var self = SelfPath;
if (IsUnder(self, dir))
{
Process.Start(new ProcessStartInfo("cmd.exe",
$"/c ping 127.0.0.1 -n 3 > nul & del /f /q \"{self}\" & del /f /q \"{Path.Combine(dir, "*.old")}\" & rmdir \"{dir}\"")
{
CreateNoWindow = true, UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden,
});
}
else
{
TryDelete(Path.Combine(dir, UninstallerName));
try { Directory.Delete(dir); } catch { } // nur wenn leer
}
progress.Report((1.0, "Fertig"));
}
/// <summary>Autostart-Einträge entfernen, die die App selbst gesetzt hat (Name vergibt Electron).</summary>
private static void RemoveAutostart(string exe)
{
if (exe.Length == 0) return;
using var run = Registry.CurrentUser.OpenSubKey(RunKey, writable: true);
if (run is null) return;
foreach (var name in run.GetValueNames())
{
if (run.GetValue(name) is string cmd && cmd.IndexOf(exe, StringComparison.OrdinalIgnoreCase) >= 0)
run.DeleteValue(name, throwOnMissingValue: false);
}
}
// ---------- Dateien ----------
/// <summary>
/// Entfernt die App-Dateien aus einem Installationsordner. Gelöscht wird nur, was nachweislich zur App gehört:
/// Einträge aus der Dateiliste der letzten Installation bzw. die Namen aus dem neuen Paket –
/// Ordner nur, wenn sie eindeutig von Electron stammen. Fremde Dateien im Ordner bleiben unangetastet.
/// </summary>
private static void RemoveAppFiles(string dir, IEnumerable<string>? extraNames)
{
if (!Directory.Exists(dir)) return;
CleanupOldFiles(dir);
var listFile = Path.Combine(dir, FileListName);
var fromList = File.Exists(listFile) ? File.ReadAllLines(listFile).Where(l => l.Trim().Length > 0).ToList() : [];
var names = fromList.Concat(extraNames ?? []).Concat([UninstallerName, FileListName])
.Distinct(StringComparer.OrdinalIgnoreCase);
foreach (var name in names)
{
if (name.IndexOfAny(['\\', '/', ':']) >= 0 || name is "." or "..") continue;
var path = Path.Combine(dir, name);
if (SamePath(path, SelfPath)) continue; // das laufende Deinstallationsprogramm räumt cmd weg
if (Directory.Exists(path))
{
bool ours = fromList.Contains(name, StringComparer.OrdinalIgnoreCase)
|| (name.Equals("resources", StringComparison.OrdinalIgnoreCase) && File.Exists(Path.Combine(path, "app.asar")))
|| (name.Equals("locales", StringComparison.OrdinalIgnoreCase) && File.Exists(Path.Combine(path, "en-US.pak")));
if (ours) Retry(path, () => Directory.Delete(path, recursive: true));
}
else if (File.Exists(path))
{
Retry(path, () => MoveAside(path));
}
}
}
/// <summary>
/// Längster Pfad im Paket. Windows erlaubt (ohne Sonderfreigabe) nur 259 Zeichen pro Pfad –
/// der Installationsordner darf deshalb höchstens <see cref="MaxDirLength"/> Zeichen lang sein.
/// </summary>
private static int? _longestEntry;
public static int MaxDirLength
{
get
{
if (_longestEntry is null)
{
try
{
using var zip = Payload.OpenArchive();
_longestEntry = zip.Entries.Count == 0 ? 0 : zip.Entries.Max(e => e.FullName.Length);
}
catch { _longestEntry = 0; }
}
return 259 - 1 - _longestEntry.Value;
}
}
private static void Retry(string path, Action action)
{
for (int attempt = 1; ; attempt++)
{
try
{
action();
return;
}
catch (PathTooLongException ex)
{
throw new IOException($"Der Installationsordner ist zu lang. Bitte einen kürzeren Ordner wählen (höchstens {MaxDirLength} Zeichen).", ex);
}
catch (Exception ex) when ((ex is IOException || ex is UnauthorizedAccessException) && attempt < 10)
{
Thread.Sleep(500);
}
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
{
throw new IOException($"„{Path.GetFileName(path)}“ wird noch verwendet. Bitte Git Manager GUI beenden " +
"(auch das Symbol im Infobereich neben der Uhr) und erneut versuchen.", ex);
}
}
}
/// <summary>
/// Vorhandene Datei löschen oder – falls sie noch geöffnet ist (Explorer, Virenscanner) – beiseite umbenennen.
/// Umbenennen klappt unter Windows auch bei geöffneten Dateien, überschreiben nicht.
/// </summary>
private static void MoveAside(string path)
{
if (!File.Exists(path)) return;
try { File.Delete(path); return; } catch { }
File.Move(path, $"{path}.{Guid.NewGuid():N}.old");
}
private static void CleanupOldFiles(string dir)
{
if (!Directory.Exists(dir)) return;
foreach (var f in Directory.GetFiles(dir, "*.old", SearchOption.AllDirectories))
try { File.Delete(f); } catch { }
}
private static void TryDelete(string path)
{
try { if (File.Exists(path)) File.Delete(path); } catch { }
}
private static void DeleteIfPointsTo(string link, string target)
{
if (!File.Exists(link) || target.Length == 0) return;
if (!SamePath(Shortcut.Target(link) ?? "", target)) return;
// Explorer/Indexdienst halten Verknüpfungen manchmal kurz fest → mehrmals versuchen
for (int i = 0; i < 10 && File.Exists(link); i++)
{
try { File.Delete(link); }
catch { Thread.Sleep(300); }
}
}
// ---------- Prozesse ----------
private static IEnumerable<Process> AppProcesses(params string?[] dirs)
{
var roots = dirs.Where(d => !string.IsNullOrEmpty(d)).Select(d => d!).ToList();
if (roots.Count == 0) yield break;
// die App (mehrere Electron-Prozesse) und das mitgelieferte Git, falls gerade ein Upload läuft
foreach (var p in Process.GetProcessesByName(ProcessName).Concat(Process.GetProcessesByName("git")))
{
string? file = null;
try { file = p.MainModule?.FileName; } catch { }
if (file is not null && roots.Any(r => IsUnder(file, r))) yield return p;
}
}
private static void CloseRunningApp(int waitMs, params string?[] dirs)
{
var until = DateTime.Now.AddMilliseconds(waitMs);
while (waitMs > 0 && DateTime.Now < until && AppProcesses(dirs).Any())
Thread.Sleep(300);
foreach (var p in AppProcesses(dirs).ToList())
{
try
{
p.Kill();
p.WaitForExit(10000);
}
catch { }
}
}
// ---------- Helfer ----------
private static bool SamePath(string a, string b)
{
try { return string.Equals(Path.GetFullPath(a).TrimEnd('\\'), Path.GetFullPath(b).TrimEnd('\\'), StringComparison.OrdinalIgnoreCase); }
catch { return false; }
}
private static bool IsUnder(string path, string dir) =>
Path.GetFullPath(path).StartsWith(Path.GetFullPath(dir).TrimEnd('\\') + "\\", StringComparison.OrdinalIgnoreCase);
private static long DirSize(string dir)
{
long sum = 0;
try
{
foreach (var f in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories))
try { sum += new FileInfo(f).Length; } catch { }
}
catch { }
return sum;
}
public static long DataSize() => Directory.Exists(DataDir) ? DirSize(DataDir) : 0;
public static string FormatSize(long bytes) => bytes switch
{
< 1024 * 1024 => $"{Math.Max(1, bytes / 1024)} KB",
< 1024L * 1024 * 1024 => $"{bytes / 1024.0 / 1024:0.#} MB",
_ => $"{bytes / 1024.0 / 1024 / 1024:0.##} GB",
};
}
+352
View File
@@ -0,0 +1,352 @@
<Window x:Class="GitManagerSetup.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Git Manager GUI Setup" Icon="app.ico" Width="780" Height="520" ResizeMode="CanMinimize"
Background="{DynamicResource LayerBrush}" Foreground="{DynamicResource TextBrush}"
FontFamily="{DynamicResource UiFont}" FontSize="14"
TextOptions.TextFormattingMode="Display" UseLayoutRounding="True"
WindowStartupLocation="CenterScreen">
<WindowChrome.WindowChrome>
<WindowChrome CaptionHeight="36" ResizeBorderThickness="0" GlassFrameThickness="0,0,0,1"
CornerRadius="0" UseAeroCaptionButtons="False"/>
</WindowChrome.WindowChrome>
<Window.Resources>
<!-- Markenfarben der Seitenleiste (in Hell und Dunkel gleich, wie die App) -->
<SolidColorBrush x:Key="SideText" Color="#F5F8FF"/>
<SolidColorBrush x:Key="SideTextSecondary" Color="#AEBDD8"/>
<SolidColorBrush x:Key="SideTextMuted" Color="#7083A4"/>
<LinearGradientBrush x:Key="BrandGradient" StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="#58D5FF" Offset="0"/>
<GradientStop Color="#5C87FF" Offset="1"/>
</LinearGradientBrush>
<Style x:Key="PageTitle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{DynamicResource DisplayFont}"/>
<Setter Property="FontSize" Value="26"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Margin" Value="0,0,0,8"/>
</Style>
<Style x:Key="Body" TargetType="TextBlock">
<Setter Property="TextWrapping" Value="Wrap"/>
<Setter Property="LineHeight" Value="21"/>
<Setter Property="Foreground" Value="{DynamicResource TextSecondaryBrush}"/>
</Style>
<Style x:Key="StepDot" TargetType="Border">
<Setter Property="Width" Value="26"/>
<Setter Property="Height" Value="26"/>
<Setter Property="CornerRadius" Value="13"/>
<Setter Property="BorderThickness" Value="1.5"/>
</Style>
<Style x:Key="StepNum" TargetType="TextBlock">
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style x:Key="StepLabel" TargetType="TextBlock">
<Setter Property="Margin" Value="12,0,0,0"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="FontSize" Value="13.5"/>
</Style>
<Style x:Key="StepLine" TargetType="Border">
<Setter Property="Width" Value="2"/>
<Setter Property="Height" Value="18"/>
<Setter Property="Margin" Value="12,3,0,3"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="CornerRadius" Value="1"/>
</Style>
<Style x:Key="FeatureIcon" TargetType="Border">
<Setter Property="Width" Value="38"/>
<Setter Property="Height" Value="38"/>
<Setter Property="CornerRadius" Value="10"/>
<Setter Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
<Setter Property="VerticalAlignment" Value="Top"/>
</Style>
<Style x:Key="FeatureGlyph" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{DynamicResource IconFont}"/>
<Setter Property="FontSize" Value="17"/>
<Setter Property="Foreground" Value="{DynamicResource AccentBrush}"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style x:Key="FeatureTitle" TargetType="TextBlock">
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="FontSize" Value="13.5"/>
</Style>
<Style x:Key="FeatureText" TargetType="TextBlock">
<Setter Property="FontSize" Value="12"/>
<Setter Property="TextWrapping" Value="Wrap"/>
<Setter Property="Margin" Value="0,2,0,0"/>
<Setter Property="Foreground" Value="{DynamicResource TextSecondaryBrush}"/>
</Style>
<Style x:Key="Card" TargetType="Border">
<Setter Property="Padding" Value="16,14"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Background" Value="{DynamicResource ControlFillBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="36"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- ===== Seitenleiste im Look der App ===== -->
<Border Grid.RowSpan="3" ClipToBounds="True">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0.6,1">
<GradientStop Color="#07111F" Offset="0"/>
<GradientStop Color="#0D1729" Offset="0.5"/>
<GradientStop Color="#13305C" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
<Grid>
<!-- Lichtschein hinter dem Logo und unten -->
<Ellipse Width="320" Height="320" Margin="-120,-110,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" IsHitTestVisible="False">
<Ellipse.Fill>
<RadialGradientBrush>
<GradientStop Color="#5558D5FF" Offset="0"/>
<GradientStop Color="#0058D5FF" Offset="1"/>
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse Width="300" Height="300" Margin="0,0,-150,-150" HorizontalAlignment="Right" VerticalAlignment="Bottom" IsHitTestVisible="False">
<Ellipse.Fill>
<RadialGradientBrush>
<GradientStop Color="#445C87FF" Offset="0"/>
<GradientStop Color="#005C87FF" Offset="1"/>
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<StackPanel Margin="30,64,24,0">
<Image Source="app.png" Width="68" Height="68" HorizontalAlignment="Left" RenderOptions.BitmapScalingMode="HighQuality">
<Image.Effect>
<DropShadowEffect Color="#58D5FF" BlurRadius="28" ShadowDepth="0" Opacity="0.6"/>
</Image.Effect>
</Image>
<TextBlock Text="Git Manager" FontFamily="{DynamicResource DisplayFont}" FontSize="26" FontWeight="SemiBold"
Foreground="{StaticResource SideText}" Margin="0,16,0,0"/>
<TextBlock x:Name="VersionText" Foreground="{StaticResource SideTextSecondary}" FontSize="12" Margin="0,2,0,0"/>
<StackPanel x:Name="Steps" Margin="0,38,0,0">
<StackPanel Orientation="Horizontal">
<Border x:Name="Dot0" Style="{StaticResource StepDot}"><TextBlock x:Name="Num0" Text="1" Style="{StaticResource StepNum}"/></Border>
<TextBlock x:Name="Label0" Text="Willkommen" Style="{StaticResource StepLabel}"/>
</StackPanel>
<Border x:Name="Line0" Style="{StaticResource StepLine}"/>
<StackPanel Orientation="Horizontal">
<Border x:Name="Dot1" Style="{StaticResource StepDot}"><TextBlock x:Name="Num1" Text="2" Style="{StaticResource StepNum}"/></Border>
<TextBlock x:Name="Label1" Text="Optionen" Style="{StaticResource StepLabel}"/>
</StackPanel>
<Border x:Name="Line1" Style="{StaticResource StepLine}"/>
<StackPanel Orientation="Horizontal">
<Border x:Name="Dot2" Style="{StaticResource StepDot}"><TextBlock x:Name="Num2" Text="3" Style="{StaticResource StepNum}"/></Border>
<TextBlock x:Name="Label2" Text="Installation" Style="{StaticResource StepLabel}"/>
</StackPanel>
</StackPanel>
</StackPanel>
<TextBlock Text="© M_Viper" FontSize="11" Foreground="{StaticResource SideTextMuted}"
VerticalAlignment="Bottom" Margin="30,0,0,20"/>
</Grid>
</Border>
<!-- Titelleiste -->
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="14,0,0,0">
<Image Source="app.ico" Width="16" Height="16" RenderOptions.BitmapScalingMode="HighQuality"/>
<TextBlock x:Name="WindowTitle" Text="Git Manager GUI Setup" FontSize="12" Margin="10,0,0,0" VerticalAlignment="Center"
Foreground="{StaticResource SideTextSecondary}"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top">
<Button Style="{StaticResource CaptionButton}" Content="&#xE921;" Click="Minimize_Click"
WindowChrome.IsHitTestVisibleInChrome="True"/>
<Button x:Name="CloseButton" Style="{StaticResource CloseCaptionButton}" Content="&#xE8BB;" Click="Cancel_Click"
WindowChrome.IsHitTestVisibleInChrome="True"/>
</StackPanel>
<!-- ===== Inhalt ===== -->
<Grid Grid.Row="1" Grid.Column="1" Margin="40,18,40,0">
<!-- 0: Willkommen -->
<StackPanel x:Name="PageWelcome">
<TextBlock Text="Willkommen beim Git Manager" Style="{StaticResource PageTitle}"/>
<TextBlock Style="{StaticResource Body}"
Text="Der Assistent richtet Git Manager GUI in wenigen Schritten ein – nur für dein Benutzerkonto und ohne Administratorrechte."/>
<Grid Margin="0,24,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="18"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<DockPanel>
<Border Style="{StaticResource FeatureIcon}" DockPanel.Dock="Left"><TextBlock Text="&#xE753;" Style="{StaticResource FeatureGlyph}"/></Border>
<StackPanel Margin="12,0,0,0">
<TextBlock Text="Gitea &amp; GitHub" Style="{StaticResource FeatureTitle}"/>
<TextBlock Text="Repos, Dateien, Releases und Commits an einem Ort" Style="{StaticResource FeatureText}"/>
</StackPanel>
</DockPanel>
<DockPanel Grid.Column="2">
<Border Style="{StaticResource FeatureIcon}" DockPanel.Dock="Left"><TextBlock Text="&#xE943;" Style="{StaticResource FeatureGlyph}"/></Border>
<StackPanel Margin="12,0,0,0">
<TextBlock Text="Git eingebaut" Style="{StaticResource FeatureTitle}"/>
<TextBlock Text="Kein zusätzliches Programm nötig" Style="{StaticResource FeatureText}"/>
</StackPanel>
</DockPanel>
<DockPanel Grid.Row="2">
<Border Style="{StaticResource FeatureIcon}" DockPanel.Dock="Left"><TextBlock Text="&#xE8C8;" Style="{StaticResource FeatureGlyph}"/></Border>
<StackPanel Margin="12,0,0,0">
<TextBlock Text="PRs &amp; Issues" Style="{StaticResource FeatureTitle}"/>
<TextBlock Text="Ansehen, kommentieren und mergen" Style="{StaticResource FeatureText}"/>
</StackPanel>
</DockPanel>
<DockPanel Grid.Row="2" Grid.Column="2">
<Border Style="{StaticResource FeatureIcon}" DockPanel.Dock="Left"><TextBlock Text="&#xE777;" Style="{StaticResource FeatureGlyph}"/></Border>
<StackPanel Margin="12,0,0,0">
<TextBlock Text="Backup" Style="{StaticResource FeatureTitle}"/>
<TextBlock Text="Alle Repos sichern und wiederherstellen" Style="{StaticResource FeatureText}"/>
</StackPanel>
</DockPanel>
</Grid>
<Border x:Name="UpdateHint" Margin="0,26,0,0" Padding="14,10" CornerRadius="8" Visibility="Collapsed"
Background="{DynamicResource AccentSubtleBrush}" BorderBrush="{DynamicResource AccentSubtleStrokeBrush}" BorderThickness="1">
<DockPanel>
<TextBlock DockPanel.Dock="Left" Text="&#xE946;" FontFamily="{DynamicResource IconFont}" FontSize="16"
Foreground="{DynamicResource AccentBrush}" Margin="0,2,12,0" VerticalAlignment="Top"/>
<TextBlock x:Name="UpdateHintText" TextWrapping="Wrap" LineHeight="20" FontSize="13"/>
</DockPanel>
</Border>
<Border x:Name="RunningHint" Margin="0,10,0,0" Padding="14,10" CornerRadius="8" Visibility="Collapsed"
Background="{DynamicResource ControlFillBrush}" BorderBrush="{DynamicResource ControlStrokeBrush}" BorderThickness="1">
<DockPanel>
<TextBlock DockPanel.Dock="Left" Text="&#xE7BA;" FontFamily="{DynamicResource IconFont}" FontSize="16"
Foreground="{DynamicResource AccentBrush}" Margin="0,2,12,0" VerticalAlignment="Top"/>
<TextBlock TextWrapping="Wrap" LineHeight="20" FontSize="13"
Text="Git Manager GUI läuft gerade und wird für die Installation automatisch beendet. Laufende Uploads gehen dabei verloren."/>
</DockPanel>
</Border>
</StackPanel>
<!-- 1: Optionen -->
<StackPanel x:Name="PageOptions" Visibility="Collapsed">
<TextBlock Text="Optionen" Style="{StaticResource PageTitle}"/>
<TextBlock Style="{StaticResource Body}" Text="Wohin soll Git Manager GUI installiert werden?"/>
<Border Style="{StaticResource Card}" Margin="0,18,0,0">
<StackPanel>
<TextBlock Text="Installationsordner" FontWeight="SemiBold" FontSize="13" Margin="0,0,0,10"/>
<DockPanel>
<Button DockPanel.Dock="Right" Content="Ändern …" Margin="8,0,0,0" Click="ChangeDir_Click"/>
<TextBox x:Name="DirBox"/>
</DockPanel>
<TextBlock Style="{StaticResource Body}" FontSize="12" LineHeight="18" Margin="0,10,0,0"
Text="Benötigt ca. 450 MB. Zugangsdaten, Einstellungen und Favoriten liegen getrennt davon und bleiben bei Updates erhalten."/>
</StackPanel>
</Border>
<Border Style="{StaticResource Card}" Margin="0,12,0,0">
<StackPanel>
<CheckBox x:Name="DesktopLinkBox" Content="Verknüpfung auf dem Desktop" IsChecked="True"/>
</StackPanel>
</Border>
</StackPanel>
<!-- 2: Fortschritt / Fertig -->
<StackPanel x:Name="PageProgress" Visibility="Collapsed">
<Border x:Name="SuccessIcon" Width="64" Height="64" CornerRadius="32" HorizontalAlignment="Left" Margin="0,4,0,20"
Background="{StaticResource BrandGradient}" RenderTransformOrigin="0.5,0.5" Visibility="Collapsed">
<Border.RenderTransform><ScaleTransform x:Name="SuccessScale" ScaleX="1" ScaleY="1"/></Border.RenderTransform>
<Border.Effect><DropShadowEffect Color="#58D5FF" BlurRadius="24" ShadowDepth="0" Opacity="0.5"/></Border.Effect>
<TextBlock Text="&#xE73E;" FontFamily="{DynamicResource IconFont}" FontSize="26" Foreground="#04101F"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<TextBlock x:Name="ProgressTitle" Text="Git Manager GUI wird installiert …" Style="{StaticResource PageTitle}"/>
<StackPanel x:Name="ProgressBarPanel" Margin="0,18,0,0">
<TextBlock x:Name="PercentText" Text="0 %" FontFamily="{DynamicResource DisplayFont}" FontSize="44" FontWeight="SemiBold"
Foreground="{DynamicResource AccentBrush}"/>
<Border x:Name="ProgressTrack" SizeChanged="ProgressTrack_SizeChanged" Height="8" CornerRadius="4" Margin="0,10,0,0" Background="{DynamicResource SubtleSelectedBrush}">
<Border x:Name="ProgressFill" HorizontalAlignment="Left" Width="0" CornerRadius="4" Background="{StaticResource BrandGradient}"/>
</Border>
</StackPanel>
<TextBlock x:Name="ProgressText" Style="{StaticResource Body}" Margin="0,12,0,0" Text="Einen Moment bitte."/>
<StackPanel x:Name="DonePanel" Visibility="Collapsed">
<CheckBox x:Name="LaunchBox" Content="Git Manager GUI jetzt starten" IsChecked="True" Margin="0,24,0,0"/>
</StackPanel>
</StackPanel>
<!-- Deinstallation -->
<StackPanel x:Name="PageUninstall" Visibility="Collapsed">
<TextBlock Text="Git Manager GUI deinstallieren" Style="{StaticResource PageTitle}"/>
<TextBlock Style="{StaticResource Body}"
Text="Git Manager GUI wird von deinem PC entfernt: Programm, Verknüpfungen, Autostart und der Eintrag in den Windows-Einstellungen."/>
<Border Style="{StaticResource Card}" Margin="0,22,0,0">
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="&#xE72E;" FontFamily="{DynamicResource IconFont}" FontSize="15" Foreground="{DynamicResource AccentBrush}"
VerticalAlignment="Center"/>
<TextBlock Text="Deine Zugangsdaten und Einstellungen bleiben erhalten" FontWeight="SemiBold" Margin="10,0,0,0" VerticalAlignment="Center"/>
</StackPanel>
<TextBlock x:Name="DataInfo" Style="{StaticResource Body}" FontSize="12.5" Margin="0,6,0,14"/>
<CheckBox x:Name="RemoveDataBox" Content="Auch Zugangsdaten, Einstellungen, Favoriten und Verlauf löschen"
Checked="RemoveDataBox_Click" Unchecked="RemoveDataBox_Click"/>
<TextBlock x:Name="RemoveDataWarning" Visibility="Collapsed" Margin="30,8,0,0" TextWrapping="Wrap" FontSize="12.5"
Foreground="{DynamicResource DangerBrush}"
Text="Die gespeicherten Tokens für Gitea und GitHub werden entfernt (sie landen im Papierkorb). Deine Repos auf dem Server, lokale Projektordner und Repo-Backups bleiben unangetastet."/>
</StackPanel>
</Border>
</StackPanel>
</Grid>
<!-- Fußleiste -->
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,14,40,24">
<Button x:Name="BackButton" Content="Zurück" Width="110" Margin="0,0,8,0" Click="Back_Click"/>
<Button x:Name="NextButton" Content="Weiter" Width="140" Margin="0,0,8,0" Style="{StaticResource AccentButton}" IsDefault="True" Click="Next_Click"/>
<Button x:Name="CancelButton" Content="Abbrechen" Width="110" Click="Cancel_Click"/>
</StackPanel>
<!-- Eigene Rückfrage im Setup-Look statt Windows-Meldungsfenster (siehe Frage()) -->
<Grid x:Name="DialogOverlay" Grid.RowSpan="3" Grid.ColumnSpan="2" Visibility="Collapsed" Background="#99000000">
<Border x:Name="DialogCard" Width="420" HorizontalAlignment="Center" VerticalAlignment="Center"
Background="{DynamicResource LayerBrush}" BorderBrush="{DynamicResource ControlStrokeBrush}" BorderThickness="1"
CornerRadius="10" Padding="26,22,22,18" Effect="{StaticResource DialogShadow}" RenderTransformOrigin="0.5,0.5">
<Border.RenderTransform><ScaleTransform x:Name="DialogScale" ScaleX="1" ScaleY="1"/></Border.RenderTransform>
<StackPanel>
<StackPanel Orientation="Horizontal">
<Border Width="34" Height="34" CornerRadius="17" Background="{DynamicResource AccentSubtleBrush}" VerticalAlignment="Top">
<TextBlock x:Name="DialogIcon" Text="&#xE7BA;" FontFamily="{DynamicResource IconFont}" FontSize="16"
Foreground="{DynamicResource AccentBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<TextBlock x:Name="DialogTitle" FontFamily="{DynamicResource DisplayFont}" FontSize="18" FontWeight="SemiBold"
VerticalAlignment="Center" Margin="12,0,0,0" TextWrapping="Wrap" MaxWidth="320"/>
</StackPanel>
<TextBlock x:Name="DialogText" TextWrapping="Wrap" LineHeight="20" Margin="46,8,0,0"
Foreground="{DynamicResource TextSecondaryBrush}"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,22,0,0">
<Button x:Name="DialogSecondary" MinWidth="120" Margin="0,0,8,0" Click="DialogSecondary_Click"/>
<Button x:Name="DialogPrimary" MinWidth="130" Style="{StaticResource AccentButton}" Click="DialogPrimary_Click"/>
</StackPanel>
</StackPanel>
</Border>
</Grid>
</Grid>
</Window>
+454
View File
@@ -0,0 +1,454 @@
using System.Diagnostics;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Threading;
namespace GitManagerSetup;
public partial class MainWindow : Window
{
private readonly SetupMode _mode;
private int _page;
private bool _busy;
private bool _done;
private string? _installedExe;
private const int PageNumOptions = 1;
private const int PageNumProgress = 2;
public MainWindow(SetupMode mode)
{
InitializeComponent();
_mode = mode;
VersionText.Text = $"Version {Installer.Version}";
var installed = Installer.InstalledDir;
DirBox.Text = installed ?? Installer.DefaultDir;
DesktopLinkBox.IsChecked = installed is null || Installer.DesktopLinkExists;
if (installed is not null)
{
UpdateHint.Visibility = Visibility.Visible;
UpdateHintText.Text = Installer.InstalledVersion == Installer.Version
? $"Git Manager GUI {Installer.Version} ist bereits installiert und wird neu installiert. Zugangsdaten und Einstellungen bleiben erhalten."
: $"Die vorhandene Installation{(Installer.InstalledVersion is { } v ? $" (Version {v})" : "")} wird durch Version {Installer.Version} ersetzt. Zugangsdaten und Einstellungen bleiben erhalten.";
}
if (Installer.IsRunning(installed)) RunningHint.Visibility = Visibility.Visible;
switch (mode)
{
case SetupMode.Uninstall:
Title = WindowTitle.Text = "Git Manager GUI deinstallieren";
Steps.Visibility = Visibility.Collapsed;
ShowOnly(PageUninstall);
NextButton.Content = "Deinstallieren";
BackButton.Visibility = Visibility.Collapsed;
LoadDataInfo();
break;
case SetupMode.Update:
// aus der App heraus: ohne Rückfragen direkt aktualisieren und danach wieder starten
Title = WindowTitle.Text = "Git Manager GUI wird aktualisiert";
Steps.Visibility = Visibility.Collapsed;
Loaded += async (_, _) => await RunInstall(DirBox.Text, DesktopLinkBox.IsChecked == true, update: true);
ShowOnly(PageProgress);
ProgressTitle.Text = "Git Manager GUI wird aktualisiert …";
BackButton.Visibility = Visibility.Hidden;
break;
default:
ShowPage(0);
break;
}
}
// ---------- Seiten ----------
private UIElement? _shownPage;
/// <summary>Seite wechseln – mit kurzem Einblenden von rechts.</summary>
private void ShowOnly(UIElement page)
{
foreach (var p in new UIElement[] { PageWelcome, PageOptions, PageProgress, PageUninstall })
p.Visibility = p == page ? Visibility.Visible : Visibility.Collapsed;
if (page == _shownPage) return;
_shownPage = page;
var ease = new CubicEase { EasingMode = EasingMode.EaseOut };
var slide = new TranslateTransform(18, 0);
page.RenderTransform = slide;
slide.BeginAnimation(TranslateTransform.XProperty, new DoubleAnimation(18, 0, TimeSpan.FromMilliseconds(260)) { EasingFunction = ease });
page.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(220)));
}
// Farben der Schritt-Anzeige in der (immer dunklen) Seitenleiste – Cyan wie in der App
private static readonly Brush StepDoneFill = Frozen(Color.FromRgb(0x58, 0xD5, 0xFF));
private static readonly Brush StepDoneGlyph = Frozen(Color.FromRgb(0x04, 0x10, 0x1F));
private static readonly Brush StepCurrentFill = Brushes.White;
private static readonly Brush StepCurrentText = Frozen(Color.FromRgb(0x13, 0x30, 0x5C));
private static readonly Brush StepUpcoming = Frozen(Color.FromRgb(0x70, 0x83, 0xA4));
private static readonly Brush StepUpcomingStroke = Frozen(Color.FromRgb(0x3A, 0x4E, 0x72));
private static readonly Brush StepDoneText = Frozen(Color.FromRgb(0xAE, 0xBD, 0xD8));
private static readonly Brush StepLineIdle = Frozen(Color.FromRgb(0x24, 0x36, 0x58));
private static Brush Frozen(Color c)
{
var b = new SolidColorBrush(c);
b.Freeze();
return b;
}
private void UpdateSteps(int page)
{
var dots = new[] { Dot0, Dot1, Dot2 };
var nums = new[] { Num0, Num1, Num2 };
var labels = new[] { Label0, Label1, Label2 };
var lines = new[] { Line0, Line1 };
var iconFont = (FontFamily)FindResource("IconFont");
var uiFont = (FontFamily)FindResource("UiFont");
for (int i = 0; i < dots.Length; i++)
{
bool done = i < page || (_done && i == page);
bool current = i == page && !done;
dots[i].Background = done ? StepDoneFill : current ? StepCurrentFill : Brushes.Transparent;
dots[i].BorderBrush = done ? StepDoneFill : current ? StepCurrentFill : StepUpcomingStroke;
nums[i].Text = done ? "" : (i + 1).ToString();
nums[i].FontFamily = done ? iconFont : uiFont;
nums[i].FontSize = done ? 11 : 12;
nums[i].Foreground = done ? StepDoneGlyph : current ? StepCurrentText : StepUpcoming;
labels[i].Foreground = current ? Brushes.White : done ? StepDoneText : StepUpcoming;
labels[i].FontWeight = current ? FontWeights.SemiBold : FontWeights.Normal;
}
for (int i = 0; i < lines.Length; i++)
lines[i].Background = i < page ? StepDoneFill : StepLineIdle;
}
private void ShowPage(int page)
{
_page = page;
ShowOnly(page switch { 0 => PageWelcome, PageNumOptions => PageOptions, _ => PageProgress });
UpdateSteps(page);
BackButton.Visibility = page == PageNumOptions ? Visibility.Visible : Visibility.Hidden;
NextButton.Content = page switch
{
PageNumOptions => "Installieren",
PageNumProgress => "Fertigstellen",
_ => "Weiter",
};
}
private void Back_Click(object sender, RoutedEventArgs e)
{
if (!_busy && _page > 0) ShowPage(_page - 1);
}
private async void Next_Click(object sender, RoutedEventArgs e)
{
if (_busy) return;
if (_mode == SetupMode.Uninstall)
{
if (_done) { Close(); return; }
await RunUninstall();
return;
}
if (_mode == SetupMode.Update)
{
Close(); // nur nach einem Fehler erreichbar
return;
}
switch (_page)
{
case 0: ShowPage(PageNumOptions); break;
case PageNumOptions:
var dir = DirBox.Text.Trim();
if (!IsFullPath(dir) || dir.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
{
await Frage("Ungültiger Ordner", "Bitte einen vollständigen Ordner angeben, z. B. C:\\Programme\\Git Manager GUI.", "OK");
return;
}
if (dir.TrimEnd('\\', '/').Length > Installer.MaxDirLength)
{
await Frage("Ordnerpfad zu lang",
$"Der Pfad hat {dir.TrimEnd('\\', '/').Length} Zeichen, Windows erlaubt hier höchstens {Installer.MaxDirLength}. " +
"Bitte einen kürzeren Ordner wählen.", "OK");
return;
}
await RunInstall(dir, DesktopLinkBox.IsChecked == true, update: false);
break;
case PageNumProgress:
// Start im Hintergrund: Process.Start kann blockieren, solange Windows
// die EXE prüft – das Fenster darf dabei nie einfrieren
if (LaunchBox.IsChecked == true)
{
_busy = true;
NextButton.IsEnabled = false;
NextButton.Content = "Wird gestartet …";
await Task.Run(StartApp);
_busy = false;
}
Close();
break;
}
}
private void ChangeDir_Click(object sender, RoutedEventArgs e)
{
using var dlg = new System.Windows.Forms.FolderBrowserDialog
{
Description = "Installationsordner wählen",
SelectedPath = Directory.Exists(DirBox.Text) ? DirBox.Text : Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
ShowNewFolderButton = true,
};
if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return;
var chosen = dlg.SelectedPath;
// wie bei üblichen Installern: Unterordner "Git Manager GUI" anhängen
DirBox.Text = Path.GetFileName(chosen.TrimEnd('\\')).Equals(Installer.AppName, StringComparison.OrdinalIgnoreCase)
? chosen : Path.Combine(chosen, Installer.AppName);
}
// ---------- Installation ----------
private async Task RunInstall(string dir, bool desktop, bool update)
{
_busy = true;
if (!update) ShowPage(PageNumProgress);
SetButtons(false);
var progress = new Progress<(double Value, string Text)>(p => SetProgress(p.Value, p.Text));
try
{
await Task.Run(() => Installer.Install(dir, desktop, progress, waitForExit: update));
_installedExe = Path.Combine(dir, Installer.ExeName);
_done = true;
if (update)
{
ShowSuccess("Update abgeschlossen", $"Git Manager GUI {Installer.Version} wird gestartet …");
StartApp();
_busy = false;
var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1.5) };
timer.Tick += (_, _) => { timer.Stop(); Close(); };
timer.Start();
return;
}
ShowSuccess("Alles bereit!", "Git Manager GUI ist installiert. Du findest es im Startmenü" + (desktop ? " und auf dem Desktop." : "."));
DonePanel.Visibility = Visibility.Visible;
ShowPage(PageNumProgress);
}
catch (Exception ex)
{
ProgressTitle.Text = update ? "Update fehlgeschlagen" : "Installation fehlgeschlagen";
ProgressBarPanel.Visibility = Visibility.Collapsed;
ProgressText.Text = ex.Message;
ProgressText.Foreground = (Brush)FindResource("DangerBrush");
NextButton.Content = "Schließen";
_installedExe = null;
}
_busy = false;
NextButton.IsEnabled = true;
CloseButton.IsEnabled = true;
BackButton.Visibility = Visibility.Hidden;
CancelButton.Visibility = Visibility.Collapsed;
}
/// <summary>Fortschritt ausblenden und den Haken mit kurzem "Aufploppen" zeigen.</summary>
private void ShowSuccess(string title, string text)
{
ProgressBarPanel.Visibility = Visibility.Collapsed;
ProgressTitle.Text = title;
ProgressText.Text = text;
SuccessIcon.Visibility = Visibility.Visible;
var pop = new DoubleAnimation(0.4, 1, TimeSpan.FromMilliseconds(420)) { EasingFunction = new BackEase { EasingMode = EasingMode.EaseOut, Amplitude = 0.5 } };
SuccessScale.BeginAnimation(ScaleTransform.ScaleXProperty, pop);
SuccessScale.BeginAnimation(ScaleTransform.ScaleYProperty, pop);
SuccessIcon.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200)));
}
private void StartApp()
{
if (_installedExe is null || !File.Exists(_installedExe)) return;
try
{
Process.Start(new ProcessStartInfo(_installedExe)
{
UseShellExecute = true,
WorkingDirectory = Path.GetDirectoryName(_installedExe)!,
});
}
catch { }
}
// ---------- Deinstallation ----------
private async void LoadDataInfo()
{
var root = Installer.DataDir;
if (!Directory.Exists(root))
{
DataInfo.Text = "Es wurden keine App-Daten gefunden.";
RemoveDataBox.IsEnabled = false;
return;
}
DataInfo.Text = $"Sie liegen in {root}. Größe wird berechnet …";
var size = await Task.Run(Installer.DataSize);
DataInfo.Text = $"Sie liegen in {root} ({Installer.FormatSize(size)}) und stehen wieder bereit, falls du Git Manager GUI später neu installierst.";
}
private void RemoveDataBox_Click(object sender, RoutedEventArgs e) =>
RemoveDataWarning.Visibility = RemoveDataBox.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
private async Task RunUninstall()
{
_busy = true;
bool removeData = RemoveDataBox.IsChecked == true;
ShowOnly(PageProgress);
ProgressTitle.Text = "Git Manager GUI wird entfernt …";
LaunchBox.Visibility = Visibility.Collapsed;
SetButtons(false);
var progress = new Progress<(double Value, string Text)>(p => SetProgress(p.Value, p.Text));
try
{
await Task.Run(() => Installer.Uninstall(removeData, progress));
ShowSuccess("Git Manager GUI wurde entfernt", removeData
? "Programm, Zugangsdaten und Einstellungen wurden entfernt (die Daten liegen im Papierkorb)."
: "Deine Zugangsdaten und Einstellungen liegen weiterhin in " + Installer.DataDir + " – falls du Git Manager GUI später wieder installierst.");
}
catch (Exception ex)
{
ProgressTitle.Text = "Deinstallation fehlgeschlagen";
ProgressBarPanel.Visibility = Visibility.Collapsed;
ProgressText.Text = ex.Message;
}
_done = true;
_busy = false;
CloseButton.IsEnabled = true;
NextButton.Content = "Schließen";
NextButton.IsEnabled = true;
CancelButton.Visibility = Visibility.Collapsed;
}
// ---------- Helfer ----------
private void SetButtons(bool enabled)
{
NextButton.IsEnabled = enabled;
BackButton.IsEnabled = enabled;
CancelButton.IsEnabled = enabled;
CloseButton.IsEnabled = enabled;
}
private void SetProgress(double value, string text)
{
if (_done) return;
ProgressText.Text = text;
PercentText.Text = $"{(int)Math.Round(value * 100)} %";
_progress = value;
AnimateProgress();
}
private double _progress;
private void AnimateProgress()
{
var target = Math.Max(0, ProgressTrack.ActualWidth * _progress);
ProgressFill.BeginAnimation(WidthProperty,
new DoubleAnimation(target, TimeSpan.FromMilliseconds(250)) { EasingFunction = new CubicEase() });
}
// Kommt der erste Wert, bevor die Seite fertig angeordnet ist (Breite 0), nachträglich richtig setzen
private void ProgressTrack_SizeChanged(object sender, SizeChangedEventArgs e) => AnimateProgress();
private static bool IsFullPath(string p) =>
p.Length >= 3 && char.IsLetter(p[0]) && p[1] == ':' && (p[2] == '\\' || p[2] == '/');
private void Minimize_Click(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
private async void Cancel_Click(object sender, RoutedEventArgs e)
{
if (_busy || _dialog is not null) return;
if (!_done && _mode == SetupMode.Install && _page > 0)
{
bool abbrechen = await Frage("Installation abbrechen?",
"Git Manager GUI wird nicht installiert. Du kannst das Setup später jederzeit erneut starten.",
"Ja, abbrechen", "Weiter installieren", "\uE897");
if (!abbrechen) return;
}
Close();
}
// ---------- eigene Rückfragen im Setup-Look ----------
private TaskCompletionSource<bool>? _dialog;
/// <summary>
/// Zeigt eine Rückfrage über dem Fenster. Mit <paramref name="zurueck"/>: zwei Knöpfe –
/// der hervorgehobene ist der sichere Weg zurück (liefert false), der andere bestätigt (true).
/// Ohne: nur ein Hinweis mit einem Knopf (liefert true).
/// </summary>
private Task<bool> Frage(string titel, string text, string bestaetigen, string? zurueck = null, string symbol = "\uE7BA")
{
_dialog?.TrySetResult(false);
_dialog = new TaskCompletionSource<bool>();
DialogTitle.Text = titel;
DialogText.Text = text;
DialogIcon.Text = symbol;
if (zurueck is null)
{
DialogSecondary.Visibility = Visibility.Collapsed;
DialogPrimary.Content = bestaetigen;
}
else
{
DialogSecondary.Visibility = Visibility.Visible;
DialogSecondary.Content = bestaetigen;
DialogPrimary.Content = zurueck;
}
DialogOverlay.Visibility = Visibility.Visible;
var ein = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(160));
DialogOverlay.BeginAnimation(OpacityProperty, ein);
var groesse = new DoubleAnimation(0.94, 1, TimeSpan.FromMilliseconds(220)) { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } };
DialogScale.BeginAnimation(ScaleTransform.ScaleXProperty, groesse);
DialogScale.BeginAnimation(ScaleTransform.ScaleYProperty, groesse);
DialogPrimary.Focus();
return _dialog.Task;
}
private void SchliesseFrage(bool ergebnis)
{
if (_dialog is null) return;
var tcs = _dialog;
_dialog = null;
DialogOverlay.Visibility = Visibility.Collapsed;
tcs.TrySetResult(ergebnis);
}
// hervorgehobener Knopf: bei Rückfragen "zurück" (false), beim reinen Hinweis "OK" (true)
private void DialogPrimary_Click(object sender, RoutedEventArgs e) =>
SchliesseFrage(DialogSecondary.Visibility != Visibility.Visible);
private void DialogSecondary_Click(object sender, RoutedEventArgs e) => SchliesseFrage(true);
protected override void OnPreviewKeyDown(System.Windows.Input.KeyEventArgs e)
{
// solange die Rückfrage offen ist: Enter = hervorgehobener Knopf, Esc = zurück
if (_dialog is not null)
{
if (e.Key == System.Windows.Input.Key.Escape) { SchliesseFrage(DialogSecondary.Visibility != Visibility.Visible); e.Handled = true; return; }
if (e.Key == System.Windows.Input.Key.Enter) { DialogPrimary_Click(this, new RoutedEventArgs()); e.Handled = true; return; }
}
base.OnPreviewKeyDown(e);
}
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
if (_busy) e.Cancel = true; // während der Installation nicht schließen
_dialog?.TrySetResult(false);
base.OnClosing(e);
}
}
+106
View File
@@ -0,0 +1,106 @@
using System.IO;
using System.IO.Compression;
using System.Text;
namespace GitManagerSetup;
/// <summary>
/// Die App liegt als ZIP hinten an der Setup-.exe angehängt:
/// [Setup.exe][ZIP][8 Byte: ZIP-Länge][8 Byte: "GITMGRPK"]
/// Windows ignoriert angehängte Daten beim Starten einer .exe.
/// </summary>
public static class Payload
{
private static readonly byte[] Magic = Encoding.ASCII.GetBytes("GITMGRPK");
private const int FooterSize = 16;
private static (long Offset, long Length)? _info;
private static bool _checked;
/// <summary>Position und Länge des ZIPs in dieser .exe, oder null (z. B. beim Deinstallationsprogramm).</summary>
public static (long Offset, long Length)? Info
{
get
{
if (_checked) return _info;
_checked = true;
try
{
using var f = File.OpenRead(Installer.SelfPath);
if (f.Length < FooterSize) return null;
f.Seek(-FooterSize, SeekOrigin.End);
var footer = new byte[FooterSize];
if (f.Read(footer, 0, FooterSize) != FooterSize) return null;
for (int i = 0; i < Magic.Length; i++)
if (footer[8 + i] != Magic[i]) return null;
long length = BitConverter.ToInt64(footer, 0);
long offset = f.Length - FooterSize - length;
if (length <= 0 || offset <= 0) return null;
_info = (offset, length);
}
catch { _info = null; }
return _info;
}
}
public static bool Exists => Info is not null;
/// <summary>Länge des eigentlichen Setup-Programms ohne Anhang.</summary>
public static long StubLength => Info?.Offset ?? new FileInfo(Installer.SelfPath).Length;
public static ZipArchive OpenArchive()
{
var info = Info ?? throw new InvalidOperationException("Die Installationsdaten fehlen in dieser Datei. Bitte das Setup neu herunterladen.");
var file = File.OpenRead(Installer.SelfPath);
return new ZipArchive(new SubStream(file, info.Offset, info.Length), ZipArchiveMode.Read, leaveOpen: false);
}
/// <summary>Nur-Lese-Ausschnitt einer Datei, damit ZipArchive den Anhang wie eine eigene Datei sieht.</summary>
private sealed class SubStream(Stream inner, long start, long length) : Stream
{
private long _pos;
public override bool CanRead => true;
public override bool CanSeek => true;
public override bool CanWrite => false;
public override long Length => length;
public override long Position
{
get => _pos;
set => _pos = Math.Max(0, Math.Min(value, length));
}
public override int Read(byte[] buffer, int offset, int count)
{
long left = length - _pos;
if (left <= 0) return 0;
if (count > left) count = (int)left;
inner.Position = start + _pos;
int n = inner.Read(buffer, offset, count);
_pos += n;
return n;
}
public override long Seek(long offset, SeekOrigin origin)
{
Position = origin switch
{
SeekOrigin.Begin => offset,
SeekOrigin.Current => _pos + offset,
_ => length + offset,
};
return _pos;
}
public override void Flush() { }
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
protected override void Dispose(bool disposing)
{
if (disposing) inner.Dispose();
base.Dispose(disposing);
}
}
}
+110
View File
@@ -0,0 +1,110 @@
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
namespace GitManagerSetup;
/// <summary>
/// Verknüpfungen (.lnk) über die Shell-Schnittstellen – inklusive AppUserModelID, damit
/// Taskleiste und Windows-Benachrichtigungen dem Git Manager wie bisher zuordnen (wie beim alten NSIS-Setup).
/// </summary>
public static class Shortcut
{
public static void Create(string link, string target, string description, string appUserModelId)
{
Directory.CreateDirectory(Path.GetDirectoryName(link)!);
var shellLink = (IShellLinkW)new CShellLink();
try
{
shellLink.SetPath(target);
shellLink.SetWorkingDirectory(Path.GetDirectoryName(target)!);
shellLink.SetIconLocation(target, 0);
shellLink.SetDescription(description);
var store = (IPropertyStore)shellLink;
var key = new PropertyKey(new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"), 5); // PKEY_AppUserModel_ID
var value = new PropVariant { vt = 31 /* VT_LPWSTR */, p = Marshal.StringToCoTaskMemUni(appUserModelId) };
try
{
store.SetValue(ref key, ref value);
store.Commit();
}
finally { Marshal.FreeCoTaskMem(value.p); }
((IPersistFile)shellLink).Save(link, true);
}
finally { Marshal.ReleaseComObject(shellLink); }
}
/// <summary>Ziel einer vorhandenen Verknüpfung, oder null.</summary>
public static string? Target(string link)
{
if (!File.Exists(link)) return null;
var shellLink = (IShellLinkW)new CShellLink();
try
{
((IPersistFile)shellLink).Load(link, 0);
var sb = new StringBuilder(1024);
shellLink.GetPath(sb, sb.Capacity, IntPtr.Zero, 0);
return sb.ToString();
}
catch { return null; }
finally { Marshal.ReleaseComObject(shellLink); }
}
// ---------- COM ----------
[ComImport, Guid("00021401-0000-0000-C000-000000000046")]
private class CShellLink { }
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("000214F9-0000-0000-C000-000000000046")]
private interface IShellLinkW
{
void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, IntPtr pfd, uint fFlags);
void GetIDList(out IntPtr ppidl);
void SetIDList(IntPtr pidl);
void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
void GetHotkey(out short pwHotkey);
void SetHotkey(short wHotkey);
void GetShowCmd(out int piShowCmd);
void SetShowCmd(int iShowCmd);
void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int piIcon);
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, uint dwReserved);
void Resolve(IntPtr hwnd, uint fFlags);
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
}
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
private interface IPropertyStore
{
void GetCount(out uint cProps);
void GetAt(uint iProp, out PropertyKey pkey);
void GetValue(ref PropertyKey key, out PropVariant pv);
void SetValue(ref PropertyKey key, ref PropVariant pv);
void Commit();
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
private struct PropertyKey(Guid fmtid, uint pid)
{
public Guid fmtid = fmtid;
public uint pid = pid;
}
// PROPVARIANT: 16 Byte (32 Bit) bzw. 24 Byte (64 Bit)
[StructLayout(LayoutKind.Sequential)]
private struct PropVariant
{
public ushort vt;
public ushort r1, r2, r3;
public IntPtr p;
public IntPtr p2;
}
}
+27
View File
@@ -0,0 +1,27 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="BaseBrush" Color="#202020"/>
<SolidColorBrush x:Key="LayerBrush" Color="#282828"/>
<SolidColorBrush x:Key="LayerStrokeBrush" Color="#1A1A1A"/>
<SolidColorBrush x:Key="SubtleHoverBrush" Color="#0FFFFFFF"/>
<SolidColorBrush x:Key="SubtlePressedBrush" Color="#0AFFFFFF"/>
<SolidColorBrush x:Key="SubtleSelectedBrush" Color="#14FFFFFF"/>
<SolidColorBrush x:Key="ControlFillBrush" Color="#2D2D2D"/>
<SolidColorBrush x:Key="ControlHoverBrush" Color="#323232"/>
<SolidColorBrush x:Key="ControlPressedBrush" Color="#272727"/>
<SolidColorBrush x:Key="ControlStrokeBrush" Color="#353535"/>
<SolidColorBrush x:Key="ControlStrongStrokeBrush" Color="#8A8A8A"/>
<SolidColorBrush x:Key="InputFocusBrush" Color="#1F1F1F"/>
<SolidColorBrush x:Key="TextBrush" Color="#FFFFFF"/>
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#C8C8C8"/>
<SolidColorBrush x:Key="TextTertiaryBrush" Color="#9A9A9A"/>
<SolidColorBrush x:Key="FlyoutBrush" Color="#2C2C2C"/>
<SolidColorBrush x:Key="FlyoutStrokeBrush" Color="#404040"/>
<SolidColorBrush x:Key="DialogFooterBrush" Color="#202020"/>
<SolidColorBrush x:Key="ScrollThumbBrush" Color="#9F9F9F"/>
<SolidColorBrush x:Key="OverlayBrush" Color="#66000000"/>
<SolidColorBrush x:Key="SuccessBrush" Color="#4CAF50"/>
<SolidColorBrush x:Key="DangerBrush" Color="#FF99A4"/>
<SolidColorBrush x:Key="FenceBrush" Color="#F21C1C1C"/>
<Color x:Key="ShadowColor">#000000</Color>
</ResourceDictionary>
+27
View File
@@ -0,0 +1,27 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="BaseBrush" Color="#F3F3F3"/>
<SolidColorBrush x:Key="LayerBrush" Color="#FAFAFA"/>
<SolidColorBrush x:Key="LayerStrokeBrush" Color="#E5E5E5"/>
<SolidColorBrush x:Key="SubtleHoverBrush" Color="#0A000000"/>
<SolidColorBrush x:Key="SubtlePressedBrush" Color="#06000000"/>
<SolidColorBrush x:Key="SubtleSelectedBrush" Color="#0F000000"/>
<SolidColorBrush x:Key="ControlFillBrush" Color="#FDFDFD"/>
<SolidColorBrush x:Key="ControlHoverBrush" Color="#F6F6F6"/>
<SolidColorBrush x:Key="ControlPressedBrush" Color="#F2F2F2"/>
<SolidColorBrush x:Key="ControlStrokeBrush" Color="#E3E3E3"/>
<SolidColorBrush x:Key="ControlStrongStrokeBrush" Color="#C4C4C4"/>
<SolidColorBrush x:Key="InputFocusBrush" Color="#FFFFFF"/>
<SolidColorBrush x:Key="TextBrush" Color="#1B1B1B"/>
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#5D5D5D"/>
<SolidColorBrush x:Key="TextTertiaryBrush" Color="#8B8B8B"/>
<SolidColorBrush x:Key="FlyoutBrush" Color="#F9F9F9"/>
<SolidColorBrush x:Key="FlyoutStrokeBrush" Color="#DCDCDC"/>
<SolidColorBrush x:Key="DialogFooterBrush" Color="#F3F3F3"/>
<SolidColorBrush x:Key="ScrollThumbBrush" Color="#8A8A8A"/>
<SolidColorBrush x:Key="OverlayBrush" Color="#4D000000"/>
<SolidColorBrush x:Key="SuccessBrush" Color="#0F7B0F"/>
<SolidColorBrush x:Key="DangerBrush" Color="#C42B1C"/>
<SolidColorBrush x:Key="FenceBrush" Color="#F5F7F7F7"/>
<Color x:Key="ShadowColor">#000000</Color>
</ResourceDictionary>
+651
View File
@@ -0,0 +1,651 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Schriften: Windows 11 nutzt "Segoe UI Variable" / "Segoe Fluent Icons", Windows 10 fällt auf Segoe UI / MDL2 zurück -->
<FontFamily x:Key="UiFont">Segoe UI Variable Text, Segoe UI</FontFamily>
<FontFamily x:Key="DisplayFont">Segoe UI Variable Display, Segoe UI</FontFamily>
<FontFamily x:Key="IconFont">Segoe Fluent Icons, Segoe MDL2 Assets</FontFamily>
<DropShadowEffect x:Key="FlyoutShadow" BlurRadius="24" ShadowDepth="6" Direction="270" Opacity="0.18"
Color="Black"/>
<DropShadowEffect x:Key="DialogShadow" BlurRadius="40" ShadowDepth="12" Direction="270" Opacity="0.28"
Color="Black"/>
<!-- ===== Buttons ===== -->
<Style TargetType="Button">
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="Background" Value="{DynamicResource ControlFillBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="14,5"/>
<Setter Property="MinHeight" Value="32"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="4" Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="True">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="Center" RecognizesAccessKey="True"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ControlHoverBrush}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ControlPressedBrush}"/>
<Setter Property="Opacity" Value="0.85"/>
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource TextBrush}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.4"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="AccentButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Foreground" Value="{DynamicResource OnAccentBrush}"/>
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1" CornerRadius="4" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="True">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="Center" RecognizesAccessKey="True"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentHoverBrush}"/>
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentHoverBrush}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Opacity" Value="0.8"/>
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource TextBrush}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.4"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Transparenter Button (Symbolleisten, Sidebar) -->
<Style x:Key="SubtleButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="9,6"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="4" Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtlePressedBrush}"/>
<Setter Property="Opacity" Value="0.75"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.35"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Titelleisten-Buttons -->
<Style x:Key="CaptionButton" TargetType="Button">
<Setter Property="Width" Value="46"/>
<Setter Property="Height" Value="32"/>
<Setter Property="FontFamily" Value="{DynamicResource IconFont}"/>
<Setter Property="FontSize" Value="10"/>
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtlePressedBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="CloseCaptionButton" TargetType="Button" BasedOn="{StaticResource CaptionButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" Background="Transparent">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#C42B1C"/>
<Setter Property="Foreground" Value="White"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#C83C30"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ===== TextBox ===== -->
<Style TargetType="TextBox">
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="CaretBrush" Value="{DynamicResource TextBrush}"/>
<Setter Property="SelectionBrush" Value="{DynamicResource AccentBrush}"/>
<Setter Property="Background" Value="{DynamicResource ControlFillBrush}"/>
<Setter Property="Padding" Value="10,6"/>
<Setter Property="MinHeight" Value="32"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Grid>
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="4"
BorderBrush="{DynamicResource ControlStrokeBrush}" BorderThickness="1"/>
<!-- Unterstrich wie bei Windows 11 -->
<Border x:Name="Line" Height="1" VerticalAlignment="Bottom" Margin="3,0" CornerRadius="0,0,4,4"
Background="{DynamicResource ControlStrongStrokeBrush}"/>
<ScrollViewer x:Name="PART_ContentHost" Margin="{TemplateBinding Padding}" VerticalAlignment="Center"
Focusable="False" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ControlHoverBrush}"/>
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource InputFocusBrush}"/>
<Setter TargetName="Line" Property="Height" Value="2"/>
<Setter TargetName="Line" Property="Margin" Value="1,0"/>
<Setter TargetName="Line" Property="Background" Value="{DynamicResource AccentBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ===== Scrollbar (schmal, wird beim Überfahren breiter) ===== -->
<Style x:Key="ScrollThumb" TargetType="Thumb">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<Border Background="{DynamicResource ScrollThumbBrush}" CornerRadius="3"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ScrollBar">
<Setter Property="Width" Value="12"/>
<Setter Property="MinWidth" Value="12"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ScrollBar">
<Grid Background="Transparent">
<Track x:Name="PART_Track" IsDirectionReversed="True">
<Track.Thumb>
<Thumb x:Name="Th" Style="{StaticResource ScrollThumb}" Width="3" HorizontalAlignment="Center" Opacity="0.7"/>
</Track.Thumb>
</Track>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Th" Property="Width" Value="6"/>
<Setter TargetName="Th" Property="Opacity" Value="1"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Orientation" Value="Horizontal">
<Setter Property="Width" Value="Auto"/>
<Setter Property="MinWidth" Value="0"/>
<Setter Property="Height" Value="12"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ScrollBar">
<Grid Background="Transparent">
<Track x:Name="PART_Track">
<Track.Thumb>
<Thumb x:Name="Th" Style="{StaticResource ScrollThumb}" Height="3" VerticalAlignment="Center" Opacity="0.7"/>
</Track.Thumb>
</Track>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Th" Property="Height" Value="6"/>
<Setter TargetName="Th" Property="Opacity" Value="1"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
<!-- ===== ToolTip ===== -->
<Style TargetType="ToolTip">
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="HasDropShadow" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToolTip">
<Border Margin="8,4,8,12" Padding="9,6" CornerRadius="4" MaxWidth="320"
Background="{DynamicResource FlyoutBrush}" BorderBrush="{DynamicResource FlyoutStrokeBrush}" BorderThickness="1"
Effect="{StaticResource FlyoutShadow}">
<ContentPresenter>
<ContentPresenter.Resources>
<Style TargetType="TextBlock">
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
</ContentPresenter.Resources>
</ContentPresenter>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ===== Kontextmenü ===== -->
<Style TargetType="ContextMenu">
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="HasDropShadow" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContextMenu">
<Border Margin="12,4,12,16" Padding="0,4" CornerRadius="8" MinWidth="200"
Background="{DynamicResource FlyoutBrush}" BorderBrush="{DynamicResource FlyoutStrokeBrush}" BorderThickness="1"
Effect="{StaticResource FlyoutShadow}">
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Cycle"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="{x:Static MenuItem.SeparatorStyleKey}" TargetType="Separator">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Separator">
<Border Height="1" Margin="0,4" Background="{DynamicResource FlyoutStrokeBrush}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="MenuItem">
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="MenuItem">
<Border x:Name="Bd" Background="Transparent" CornerRadius="4" Margin="4,1" Padding="11,0" Height="34">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="28"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ContentPresenter ContentSource="Icon" VerticalAlignment="Center" HorizontalAlignment="Left"
TextElement.FontFamily="{DynamicResource IconFont}" TextElement.FontSize="16"/>
<ContentPresenter Grid.Column="1" ContentSource="Header" VerticalAlignment="Center" RecognizesAccessKey="True"/>
<TextBlock x:Name="Gesture" Grid.Column="2" Text="{TemplateBinding InputGestureText}" FontSize="12"
VerticalAlignment="Center" Margin="24,0,0,0" Foreground="{DynamicResource TextTertiaryBrush}"/>
<TextBlock x:Name="Arrow" Grid.Column="2" Text="&#xE76C;" FontFamily="{DynamicResource IconFont}" FontSize="11"
VerticalAlignment="Center" Margin="24,0,0,0" Visibility="Collapsed"
Foreground="{DynamicResource TextSecondaryBrush}"/>
<Popup x:Name="PART_Popup" Placement="Right" HorizontalOffset="-4" VerticalOffset="-12"
IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}"
AllowsTransparency="True" Focusable="False" PopupAnimation="Fade">
<Border Margin="12,4,12,16" Padding="0,4" CornerRadius="8" MinWidth="180"
Background="{DynamicResource FlyoutBrush}" BorderBrush="{DynamicResource FlyoutStrokeBrush}"
BorderThickness="1" Effect="{StaticResource FlyoutShadow}">
<ScrollViewer MaxHeight="420" VerticalScrollBarVisibility="Auto">
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Cycle"/>
</ScrollViewer>
</Border>
</Popup>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
</Trigger>
<Trigger Property="Role" Value="SubmenuHeader">
<Setter TargetName="Arrow" Property="Visibility" Value="Visible"/>
<Setter TargetName="Gesture" Property="Visibility" Value="Collapsed"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.4"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ===== Listen ===== -->
<Style TargetType="ListBox">
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBox">
<ScrollViewer Focusable="False" Padding="{TemplateBinding Padding}">
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Navigationseintrag in der Seitenleiste -->
<Style x:Key="NavItem" TargetType="ListBoxItem">
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Height" Value="38"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Grid Margin="4,1" Background="Transparent">
<Border x:Name="Bd" CornerRadius="4" BorderThickness="1" BorderBrush="Transparent" Background="Transparent"/>
<Rectangle x:Name="Pill" Width="3" Height="16" RadiusX="1.5" RadiusY="1.5" HorizontalAlignment="Left"
Fill="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
<ContentPresenter Margin="14,0,12,0" VerticalAlignment="Center"/>
<!-- Einfügelinie beim Umsortieren -->
<Border x:Name="InsertTop" Height="2" CornerRadius="1" VerticalAlignment="Top" Margin="6,-2,6,0"
Background="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
<Border x:Name="InsertBottom" Height="2" CornerRadius="1" VerticalAlignment="Bottom" Margin="6,0,6,-2"
Background="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleSelectedBrush}"/>
<Setter TargetName="Pill" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="Tag" Value="before">
<Setter TargetName="InsertTop" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="Tag" Value="after">
<Setter TargetName="InsertBottom" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="Tag" Value="drop">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Icon-Kachel -->
<Style x:Key="TileItem" TargetType="ListBoxItem">
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border x:Name="Bd" Margin="2" CornerRadius="6" BorderThickness="1" BorderBrush="Transparent" Background="Transparent">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentSubtleStrokeBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ===== Listenansicht-Zeile ===== -->
<Style x:Key="RowItem" TargetType="ListBoxItem">
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Grid Margin="0,1">
<Border x:Name="Bd" CornerRadius="4" Background="Transparent"/>
<Rectangle x:Name="Pill" Width="3" Height="16" RadiusX="1.5" RadiusY="1.5" HorizontalAlignment="Left"
Fill="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
<ContentPresenter/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
<Setter TargetName="Pill" Property="Visibility" Value="Visible"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ===== Karte (Einstellungen) ===== -->
<Style x:Key="Card" TargetType="Border">
<Setter Property="Background" Value="{DynamicResource ControlFillBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="6"/>
<Setter Property="Padding" Value="18,14"/>
<Setter Property="Margin" Value="0,0,0,4"/>
</Style>
<Style x:Key="SectionTitle" TargetType="TextBlock">
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Margin" Value="2,24,0,8"/>
</Style>
<!-- ===== Kippschalter (Windows 11) ===== -->
<Style x:Key="ToggleSwitch" TargetType="CheckBox">
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CheckBox">
<StackPanel Orientation="Horizontal" Background="Transparent">
<TextBlock x:Name="State" Text="Aus" VerticalAlignment="Center" Margin="0,0,12,0" MinWidth="24" TextAlignment="Right"/>
<Grid Width="40" Height="20">
<Border x:Name="Track" CornerRadius="10" BorderThickness="1"
Background="{DynamicResource SubtleSelectedBrush}" BorderBrush="{DynamicResource ControlStrongStrokeBrush}"/>
<Ellipse x:Name="Knob" Width="12" Height="12" HorizontalAlignment="Left" Margin="4,0,0,0"
Fill="{DynamicResource TextSecondaryBrush}"/>
</Grid>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Track" Property="Background" Value="{DynamicResource AccentBrush}"/>
<Setter TargetName="Track" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
<Setter TargetName="Knob" Property="Fill" Value="{DynamicResource OnAccentBrush}"/>
<Setter TargetName="Knob" Property="HorizontalAlignment" Value="Right"/>
<Setter TargetName="Knob" Property="Margin" Value="0,0,4,0"/>
<Setter TargetName="State" Property="Text" Value="Ein"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Knob" Property="Width" Value="14"/>
<Setter TargetName="Knob" Property="Height" Value="14"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.4"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ===== Kontrollkästchen ===== -->
<Style TargetType="CheckBox">
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CheckBox">
<StackPanel Orientation="Horizontal" Background="Transparent">
<Border x:Name="Box" Width="20" Height="20" CornerRadius="4" BorderThickness="1" VerticalAlignment="Center"
Background="{DynamicResource ControlFillBrush}" BorderBrush="{DynamicResource ControlStrongStrokeBrush}">
<TextBlock x:Name="Check" Text="&#xE73E;" FontFamily="{DynamicResource IconFont}" FontSize="12"
Foreground="{DynamicResource OnAccentBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"
Visibility="Collapsed"/>
</Border>
<ContentPresenter Margin="10,0,0,0" VerticalAlignment="Center"/>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Box" Property="Background" Value="{DynamicResource AccentBrush}"/>
<Setter TargetName="Box" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
<Setter TargetName="Check" Property="Visibility" Value="Visible"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Box" Property="Opacity" Value="0.85"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.4"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ===== Segment-Auswahl (z. B. Hell | Dunkel | System) ===== -->
<Style x:Key="SegmentRadio" TargetType="RadioButton">
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Padding" Value="14,6"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Border x:Name="Bd" CornerRadius="4" Padding="{TemplateBinding Padding}" Margin="2" Background="Transparent">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource OnAccentBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SegmentGroup" TargetType="Border">
<Setter Property="Background" Value="{DynamicResource SubtlePressedBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="6"/>
<Setter Property="Padding" Value="1"/>
</Style>
<!-- ===== Farb- und Symbolauswahl ===== -->
<Style x:Key="SwatchRadio" TargetType="RadioButton">
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Grid Width="34" Height="34" Margin="0,0,6,6" Background="Transparent">
<Ellipse x:Name="Ring" Stroke="{DynamicResource TextBrush}" StrokeThickness="2" Visibility="Collapsed"/>
<Ellipse Margin="5" Fill="{TemplateBinding Background}" Stroke="{DynamicResource ControlStrokeBrush}"/>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Ring" Property="Visibility" Value="Visible"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="GlyphRadio" TargetType="RadioButton">
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="FontFamily" Value="{DynamicResource IconFont}"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Border x:Name="Bd" Width="36" Height="36" Margin="0,0,4,4" CornerRadius="4" BorderThickness="1"
BorderBrush="Transparent" Background="Transparent">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="GitManager.Setup"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<!-- Installation nur für den aktuellen Benutzer → keine Administratorrechte -->
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>
@@ -0,0 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
Binary file not shown.
+77
View File
@@ -0,0 +1,77 @@
#pragma checksum "..\..\..\App.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "D722851FC43B542A95C365A4D89BC7E17AC3C32D"
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Controls.Ribbon;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Forms.Integration;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Effects;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using System.Windows.Media.TextFormatting;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Shell;
namespace GitManagerSetup {
/// <summary>
/// App
/// </summary>
public partial class App : System.Windows.Application {
private bool _contentLoaded;
/// <summary>
/// InitializeComponent
/// </summary>
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.13.0")]
public void InitializeComponent() {
if (_contentLoaded) {
return;
}
_contentLoaded = true;
System.Uri resourceLocater = new System.Uri("/Git-Manager-GUI-Setup;V0.0.0.0;component/app.xaml", System.UriKind.Relative);
#line 1 "..\..\..\App.xaml"
System.Windows.Application.LoadComponent(this, resourceLocater);
#line default
#line hidden
}
/// <summary>
/// Application Entry Point.
/// </summary>
[System.STAThreadAttribute()]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.13.0")]
public static void Main() {
GitManagerSetup.App app = new GitManagerSetup.App();
app.InitializeComponent();
app.Run();
}
}
}
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
</startup>
</configuration>
@@ -0,0 +1,20 @@
Git-Manager-GUI-Setup
0.0.0.0
winexe
C#
.cs
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Debug\net48\
GitManagerSetup
none
false
TRACE;DEBUG;NETFRAMEWORK;NET48;NET30_OR_GREATER;NET35_OR_GREATER;NET40_OR_GREATER;NET45_OR_GREATER;NET451_OR_GREATER;NET452_OR_GREATER;NET46_OR_GREATER;NET461_OR_GREATER;NET462_OR_GREATER;NET47_OR_GREATER;NET471_OR_GREATER;NET472_OR_GREATER;NET48_OR_GREATER
C:\Users\Viper\Desktop\git-manager-gui\installer\App.xaml
41549299691
8-482891414
23-602304815
MainWindow.xaml;Themes\Colors.Dark.xaml;Themes\Colors.Light.xaml;Themes\Controls.xaml;
False
@@ -0,0 +1,25 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("M_Viper")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyCopyrightAttribute("Copyright (c) M_Viper")]
[assembly: System.Reflection.AssemblyDescriptionAttribute("Installiert Git Manager GUI")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("0.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("0.0.0")]
[assembly: System.Reflection.AssemblyProductAttribute("Git Manager GUI Setup")]
[assembly: System.Reflection.AssemblyTitleAttribute("Git Manager GUI Setup")]
[assembly: System.Reflection.AssemblyVersionAttribute("0.0.0.0")]
[assembly: System.Resources.NeutralResourcesLanguageAttribute("de-DE")]
// Von der MSBuild WriteCodeFragment-Klasse generiert.
@@ -0,0 +1 @@
9c3aaae1d47c807c810117538379b6f2752577cee8cc71a0a6017da12833e788
@@ -0,0 +1,14 @@
is_global = true
build_property.ApplicationManifest = app.manifest
build_property.StartupObject =
build_property.ApplicationDefaultFont =
build_property.ApplicationHighDpiMode =
build_property.ApplicationUseCompatibleTextRendering =
build_property.ApplicationVisualStyles =
build_property.RootNamespace = GitManagerSetup
build_property.ProjectDir = C:\Users\Viper\Desktop\git-manager-gui\installer\
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =
build_property.CsWinRTUseWindowsUIXamlProjections = false
build_property.EffectiveAnalysisLevelStyle =
build_property.EnableCodeStyleSeverity =
@@ -0,0 +1,6 @@
// <auto-generated/>
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
Binary file not shown.
+676
View File
@@ -0,0 +1,676 @@
#pragma checksum "..\..\..\MainWindow.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "248163D303017091F6C4A571FD0B5DF4D5DBF414"
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Controls.Ribbon;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Forms.Integration;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Effects;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using System.Windows.Media.TextFormatting;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Shell;
namespace GitManagerSetup {
/// <summary>
/// MainWindow
/// </summary>
public partial class MainWindow : System.Windows.Window, System.Windows.Markup.IComponentConnector {
#line 140 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock VersionText;
#line default
#line hidden
#line 142 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.StackPanel Steps;
#line default
#line hidden
#line 144 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border Dot0;
#line default
#line hidden
#line 144 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock Num0;
#line default
#line hidden
#line 145 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock Label0;
#line default
#line hidden
#line 147 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border Line0;
#line default
#line hidden
#line 149 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border Dot1;
#line default
#line hidden
#line 149 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock Num1;
#line default
#line hidden
#line 150 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock Label1;
#line default
#line hidden
#line 152 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border Line1;
#line default
#line hidden
#line 154 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border Dot2;
#line default
#line hidden
#line 154 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock Num2;
#line default
#line hidden
#line 155 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock Label2;
#line default
#line hidden
#line 168 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock WindowTitle;
#line default
#line hidden
#line 174 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Button CloseButton;
#line default
#line hidden
#line 182 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.StackPanel PageWelcome;
#line default
#line hidden
#line 229 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border UpdateHint;
#line default
#line hidden
#line 234 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock UpdateHintText;
#line default
#line hidden
#line 237 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border RunningHint;
#line default
#line hidden
#line 249 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.StackPanel PageOptions;
#line default
#line hidden
#line 258 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox DirBox;
#line default
#line hidden
#line 267 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.CheckBox DesktopLinkBox;
#line default
#line hidden
#line 273 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.StackPanel PageProgress;
#line default
#line hidden
#line 274 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border SuccessIcon;
#line default
#line hidden
#line 276 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Media.ScaleTransform SuccessScale;
#line default
#line hidden
#line 281 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock ProgressTitle;
#line default
#line hidden
#line 283 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.StackPanel ProgressBarPanel;
#line default
#line hidden
#line 284 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock PercentText;
#line default
#line hidden
#line 286 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border ProgressTrack;
#line default
#line hidden
#line 287 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border ProgressFill;
#line default
#line hidden
#line 290 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock ProgressText;
#line default
#line hidden
#line 292 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.StackPanel DonePanel;
#line default
#line hidden
#line 293 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.CheckBox LaunchBox;
#line default
#line hidden
#line 298 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.StackPanel PageUninstall;
#line default
#line hidden
#line 309 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock DataInfo;
#line default
#line hidden
#line 310 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.CheckBox RemoveDataBox;
#line default
#line hidden
#line 312 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock RemoveDataWarning;
#line default
#line hidden
#line 322 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Button BackButton;
#line default
#line hidden
#line 323 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Button NextButton;
#line default
#line hidden
#line 324 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Button CancelButton;
#line default
#line hidden
#line 328 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Grid DialogOverlay;
#line default
#line hidden
#line 329 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border DialogCard;
#line default
#line hidden
#line 332 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Media.ScaleTransform DialogScale;
#line default
#line hidden
#line 336 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock DialogIcon;
#line default
#line hidden
#line 339 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock DialogTitle;
#line default
#line hidden
#line 342 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock DialogText;
#line default
#line hidden
#line 345 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Button DialogSecondary;
#line default
#line hidden
#line 346 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Button DialogPrimary;
#line default
#line hidden
private bool _contentLoaded;
/// <summary>
/// InitializeComponent
/// </summary>
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.13.0")]
public void InitializeComponent() {
if (_contentLoaded) {
return;
}
_contentLoaded = true;
System.Uri resourceLocater = new System.Uri("/Git-Manager-GUI-Setup;V0.0.0.0;component/mainwindow.xaml", System.UriKind.Relative);
#line 1 "..\..\..\MainWindow.xaml"
System.Windows.Application.LoadComponent(this, resourceLocater);
#line default
#line hidden
}
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.13.0")]
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
void System.Windows.Markup.IComponentConnector.Connect(int connectionId, object target) {
switch (connectionId)
{
case 1:
this.VersionText = ((System.Windows.Controls.TextBlock)(target));
return;
case 2:
this.Steps = ((System.Windows.Controls.StackPanel)(target));
return;
case 3:
this.Dot0 = ((System.Windows.Controls.Border)(target));
return;
case 4:
this.Num0 = ((System.Windows.Controls.TextBlock)(target));
return;
case 5:
this.Label0 = ((System.Windows.Controls.TextBlock)(target));
return;
case 6:
this.Line0 = ((System.Windows.Controls.Border)(target));
return;
case 7:
this.Dot1 = ((System.Windows.Controls.Border)(target));
return;
case 8:
this.Num1 = ((System.Windows.Controls.TextBlock)(target));
return;
case 9:
this.Label1 = ((System.Windows.Controls.TextBlock)(target));
return;
case 10:
this.Line1 = ((System.Windows.Controls.Border)(target));
return;
case 11:
this.Dot2 = ((System.Windows.Controls.Border)(target));
return;
case 12:
this.Num2 = ((System.Windows.Controls.TextBlock)(target));
return;
case 13:
this.Label2 = ((System.Windows.Controls.TextBlock)(target));
return;
case 14:
this.WindowTitle = ((System.Windows.Controls.TextBlock)(target));
return;
case 15:
#line 172 "..\..\..\MainWindow.xaml"
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.Minimize_Click);
#line default
#line hidden
return;
case 16:
this.CloseButton = ((System.Windows.Controls.Button)(target));
#line 174 "..\..\..\MainWindow.xaml"
this.CloseButton.Click += new System.Windows.RoutedEventHandler(this.Cancel_Click);
#line default
#line hidden
return;
case 17:
this.PageWelcome = ((System.Windows.Controls.StackPanel)(target));
return;
case 18:
this.UpdateHint = ((System.Windows.Controls.Border)(target));
return;
case 19:
this.UpdateHintText = ((System.Windows.Controls.TextBlock)(target));
return;
case 20:
this.RunningHint = ((System.Windows.Controls.Border)(target));
return;
case 21:
this.PageOptions = ((System.Windows.Controls.StackPanel)(target));
return;
case 22:
#line 257 "..\..\..\MainWindow.xaml"
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.ChangeDir_Click);
#line default
#line hidden
return;
case 23:
this.DirBox = ((System.Windows.Controls.TextBox)(target));
return;
case 24:
this.DesktopLinkBox = ((System.Windows.Controls.CheckBox)(target));
return;
case 25:
this.PageProgress = ((System.Windows.Controls.StackPanel)(target));
return;
case 26:
this.SuccessIcon = ((System.Windows.Controls.Border)(target));
return;
case 27:
this.SuccessScale = ((System.Windows.Media.ScaleTransform)(target));
return;
case 28:
this.ProgressTitle = ((System.Windows.Controls.TextBlock)(target));
return;
case 29:
this.ProgressBarPanel = ((System.Windows.Controls.StackPanel)(target));
return;
case 30:
this.PercentText = ((System.Windows.Controls.TextBlock)(target));
return;
case 31:
this.ProgressTrack = ((System.Windows.Controls.Border)(target));
#line 286 "..\..\..\MainWindow.xaml"
this.ProgressTrack.SizeChanged += new System.Windows.SizeChangedEventHandler(this.ProgressTrack_SizeChanged);
#line default
#line hidden
return;
case 32:
this.ProgressFill = ((System.Windows.Controls.Border)(target));
return;
case 33:
this.ProgressText = ((System.Windows.Controls.TextBlock)(target));
return;
case 34:
this.DonePanel = ((System.Windows.Controls.StackPanel)(target));
return;
case 35:
this.LaunchBox = ((System.Windows.Controls.CheckBox)(target));
return;
case 36:
this.PageUninstall = ((System.Windows.Controls.StackPanel)(target));
return;
case 37:
this.DataInfo = ((System.Windows.Controls.TextBlock)(target));
return;
case 38:
this.RemoveDataBox = ((System.Windows.Controls.CheckBox)(target));
#line 311 "..\..\..\MainWindow.xaml"
this.RemoveDataBox.Checked += new System.Windows.RoutedEventHandler(this.RemoveDataBox_Click);
#line default
#line hidden
#line 311 "..\..\..\MainWindow.xaml"
this.RemoveDataBox.Unchecked += new System.Windows.RoutedEventHandler(this.RemoveDataBox_Click);
#line default
#line hidden
return;
case 39:
this.RemoveDataWarning = ((System.Windows.Controls.TextBlock)(target));
return;
case 40:
this.BackButton = ((System.Windows.Controls.Button)(target));
#line 322 "..\..\..\MainWindow.xaml"
this.BackButton.Click += new System.Windows.RoutedEventHandler(this.Back_Click);
#line default
#line hidden
return;
case 41:
this.NextButton = ((System.Windows.Controls.Button)(target));
#line 323 "..\..\..\MainWindow.xaml"
this.NextButton.Click += new System.Windows.RoutedEventHandler(this.Next_Click);
#line default
#line hidden
return;
case 42:
this.CancelButton = ((System.Windows.Controls.Button)(target));
#line 324 "..\..\..\MainWindow.xaml"
this.CancelButton.Click += new System.Windows.RoutedEventHandler(this.Cancel_Click);
#line default
#line hidden
return;
case 43:
this.DialogOverlay = ((System.Windows.Controls.Grid)(target));
return;
case 44:
this.DialogCard = ((System.Windows.Controls.Border)(target));
return;
case 45:
this.DialogScale = ((System.Windows.Media.ScaleTransform)(target));
return;
case 46:
this.DialogIcon = ((System.Windows.Controls.TextBlock)(target));
return;
case 47:
this.DialogTitle = ((System.Windows.Controls.TextBlock)(target));
return;
case 48:
this.DialogText = ((System.Windows.Controls.TextBlock)(target));
return;
case 49:
this.DialogSecondary = ((System.Windows.Controls.Button)(target));
#line 345 "..\..\..\MainWindow.xaml"
this.DialogSecondary.Click += new System.Windows.RoutedEventHandler(this.DialogSecondary_Click);
#line default
#line hidden
return;
case 50:
this.DialogPrimary = ((System.Windows.Controls.Button)(target));
#line 346 "..\..\..\MainWindow.xaml"
this.DialogPrimary.Click += new System.Windows.RoutedEventHandler(this.DialogPrimary_Click);
#line default
#line hidden
return;
}
this._contentLoaded = true;
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,59 @@
{
"format": 1,
"restore": {
"C:\\Users\\Viper\\Desktop\\git-manager-gui\\installer\\GitManagerSetup.csproj": {}
},
"projects": {
"C:\\Users\\Viper\\Desktop\\git-manager-gui\\installer\\GitManagerSetup.csproj": {
"version": "0.0.0",
"restore": {
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\git-manager-gui\\installer\\GitManagerSetup.csproj",
"projectName": "Git-Manager-GUI-Setup",
"projectPath": "C:\\Users\\Viper\\Desktop\\git-manager-gui\\installer\\GitManagerSetup.csproj",
"packagesPath": "C:\\Users\\Viper\\.nuget\\packages\\",
"outputPath": "C:\\Users\\Viper\\Desktop\\git-manager-gui\\installer\\obj\\",
"projectStyle": "PackageReference",
"configFilePaths": [
"C:\\Users\\Viper\\AppData\\Roaming\\NuGet\\NuGet.Config"
],
"originalTargetFrameworks": [
"net48"
],
"sources": {
"https://api.nuget.org/v3/index.json": {}
},
"frameworks": {
"net48": {
"targetAlias": "net48",
"projectReferences": {}
}
},
"warningProperties": {
"warnAsError": [
"NU1605"
]
},
"restoreAuditProperties": {
"enableAudit": "true",
"auditLevel": "low",
"auditMode": "direct"
},
"SdkAnalysisLevel": "10.0.100"
},
"frameworks": {
"net48": {
"targetAlias": "net48",
"dependencies": {
"Microsoft.NETFramework.ReferenceAssemblies": {
"suppressParent": "All",
"target": "Package",
"version": "[1.0.3, )",
"autoReferenced": true
}
},
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.113\\RuntimeIdentifierGraph.json"
}
}
}
}
}
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\Viper\.nuget\packages\</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="C:\Users\Viper\.nuget\packages\" />
</ItemGroup>
</Project>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<Import Project="$(NuGetPackageRoot)microsoft.netframework.referenceassemblies.net48\1.0.3\build\Microsoft.NETFramework.ReferenceAssemblies.net48.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.netframework.referenceassemblies.net48\1.0.3\build\Microsoft.NETFramework.ReferenceAssemblies.net48.targets')" />
</ImportGroup>
</Project>
@@ -0,0 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
Binary file not shown.
+77
View File
@@ -0,0 +1,77 @@
#pragma checksum "..\..\..\App.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "D722851FC43B542A95C365A4D89BC7E17AC3C32D"
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Controls.Ribbon;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Forms.Integration;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Effects;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using System.Windows.Media.TextFormatting;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Shell;
namespace GitManagerSetup {
/// <summary>
/// App
/// </summary>
public partial class App : System.Windows.Application {
private bool _contentLoaded;
/// <summary>
/// InitializeComponent
/// </summary>
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.13.0")]
public void InitializeComponent() {
if (_contentLoaded) {
return;
}
_contentLoaded = true;
System.Uri resourceLocater = new System.Uri("/Git-Manager-GUI-Setup;component/app.xaml", System.UriKind.Relative);
#line 1 "..\..\..\App.xaml"
System.Windows.Application.LoadComponent(this, resourceLocater);
#line default
#line hidden
}
/// <summary>
/// Application Entry Point.
/// </summary>
[System.STAThreadAttribute()]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.13.0")]
public static void Main() {
GitManagerSetup.App app = new GitManagerSetup.App();
app.InitializeComponent();
app.Run();
}
}
}
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
</startup>
</configuration>
@@ -0,0 +1,20 @@
Git-Manager-GUI-Setup
winexe
C#
.cs
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\
GitManagerSetup
none
false
TRACE;RELEASE;NETFRAMEWORK;NET48;NET30_OR_GREATER;NET35_OR_GREATER;NET40_OR_GREATER;NET45_OR_GREATER;NET451_OR_GREATER;NET452_OR_GREATER;NET46_OR_GREATER;NET461_OR_GREATER;NET462_OR_GREATER;NET47_OR_GREATER;NET471_OR_GREATER;NET472_OR_GREATER;NET48_OR_GREATER
C:\Users\Viper\Desktop\git-manager-gui\installer\App.xaml
41549299691
6-1705775420
23-602304815
MainWindow.xaml;Themes\Colors.Dark.xaml;Themes\Colors.Light.xaml;Themes\Controls.xaml;
False
@@ -0,0 +1,25 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("M_Viper")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
[assembly: System.Reflection.AssemblyCopyrightAttribute("Copyright (c) M_Viper")]
[assembly: System.Reflection.AssemblyDescriptionAttribute("Installiert Git Manager GUI")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("2.2.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("2.2.0")]
[assembly: System.Reflection.AssemblyProductAttribute("Git Manager GUI Setup")]
[assembly: System.Reflection.AssemblyTitleAttribute("Git Manager GUI Setup")]
[assembly: System.Reflection.AssemblyVersionAttribute("2.2.0.0")]
[assembly: System.Resources.NeutralResourcesLanguageAttribute("de-DE")]
// Von der MSBuild WriteCodeFragment-Klasse generiert.
@@ -0,0 +1 @@
62f4b9ac7b1430e3a52ef9dfe2e8422ccd7cc5a7e41145550122532f56db4abe
@@ -0,0 +1,14 @@
is_global = true
build_property.ApplicationManifest = app.manifest
build_property.StartupObject =
build_property.ApplicationDefaultFont =
build_property.ApplicationHighDpiMode =
build_property.ApplicationUseCompatibleTextRendering =
build_property.ApplicationVisualStyles =
build_property.RootNamespace = GitManagerSetup
build_property.ProjectDir = C:\Users\Viper\Desktop\git-manager-gui\installer\
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =
build_property.CsWinRTUseWindowsUIXamlProjections = false
build_property.EffectiveAnalysisLevelStyle =
build_property.EnableCodeStyleSeverity =
@@ -0,0 +1,6 @@
// <auto-generated/>
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
@@ -0,0 +1 @@
4e41dc5e237f26559d3c4bd8208c813ba09bdba804615d78813773d6b0be3d1d
@@ -0,0 +1,19 @@
C:\Users\Viper\AppData\Local\Temp\claude\G--\bba0d154-b599-43ca-80e6-538e01f4f870\scratchpad\setupbuild\Git-Manager-GUI-Setup.exe.config
C:\Users\Viper\AppData\Local\Temp\claude\G--\bba0d154-b599-43ca-80e6-538e01f4f870\scratchpad\setupbuild\Git-Manager-GUI-Setup.exe
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\GitManagerSetup.csproj.AssemblyReference.cache
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\MainWindow.baml
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\Themes\Colors.Dark.baml
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\Themes\Colors.Light.baml
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\Themes\Controls.baml
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\App.baml
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\MainWindow.g.cs
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\App.g.cs
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\Git-Manager-GUI-Setup_MarkupCompile.cache
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\Git-Manager-GUI-Setup.g.resources
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\GitManagerSetup.GeneratedMSBuildEditorConfig.editorconfig
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\GitManagerSetup.AssemblyInfoInputs.cache
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\GitManagerSetup.AssemblyInfo.cs
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\GitManagerSetup.csproj.CoreCompileInputs.cache
C:\Users\Viper\Desktop\git-manager-gui\installer\obj\Release\net48\Git-Manager-GUI-Setup.exe
C:\Users\Viper\Desktop\git-manager-gui\dist\.setup\stub\Git-Manager-GUI-Setup.exe.config
C:\Users\Viper\Desktop\git-manager-gui\dist\.setup\stub\Git-Manager-GUI-Setup.exe
Binary file not shown.
+676
View File
@@ -0,0 +1,676 @@
#pragma checksum "..\..\..\MainWindow.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "248163D303017091F6C4A571FD0B5DF4D5DBF414"
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Automation;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Controls.Ribbon;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Forms.Integration;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Effects;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using System.Windows.Media.TextFormatting;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Shell;
namespace GitManagerSetup {
/// <summary>
/// MainWindow
/// </summary>
public partial class MainWindow : System.Windows.Window, System.Windows.Markup.IComponentConnector {
#line 140 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock VersionText;
#line default
#line hidden
#line 142 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.StackPanel Steps;
#line default
#line hidden
#line 144 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border Dot0;
#line default
#line hidden
#line 144 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock Num0;
#line default
#line hidden
#line 145 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock Label0;
#line default
#line hidden
#line 147 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border Line0;
#line default
#line hidden
#line 149 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border Dot1;
#line default
#line hidden
#line 149 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock Num1;
#line default
#line hidden
#line 150 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock Label1;
#line default
#line hidden
#line 152 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border Line1;
#line default
#line hidden
#line 154 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border Dot2;
#line default
#line hidden
#line 154 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock Num2;
#line default
#line hidden
#line 155 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock Label2;
#line default
#line hidden
#line 168 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock WindowTitle;
#line default
#line hidden
#line 174 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Button CloseButton;
#line default
#line hidden
#line 182 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.StackPanel PageWelcome;
#line default
#line hidden
#line 229 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border UpdateHint;
#line default
#line hidden
#line 234 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock UpdateHintText;
#line default
#line hidden
#line 237 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border RunningHint;
#line default
#line hidden
#line 249 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.StackPanel PageOptions;
#line default
#line hidden
#line 258 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox DirBox;
#line default
#line hidden
#line 267 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.CheckBox DesktopLinkBox;
#line default
#line hidden
#line 273 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.StackPanel PageProgress;
#line default
#line hidden
#line 274 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border SuccessIcon;
#line default
#line hidden
#line 276 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Media.ScaleTransform SuccessScale;
#line default
#line hidden
#line 281 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock ProgressTitle;
#line default
#line hidden
#line 283 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.StackPanel ProgressBarPanel;
#line default
#line hidden
#line 284 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock PercentText;
#line default
#line hidden
#line 286 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border ProgressTrack;
#line default
#line hidden
#line 287 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border ProgressFill;
#line default
#line hidden
#line 290 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock ProgressText;
#line default
#line hidden
#line 292 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.StackPanel DonePanel;
#line default
#line hidden
#line 293 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.CheckBox LaunchBox;
#line default
#line hidden
#line 298 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.StackPanel PageUninstall;
#line default
#line hidden
#line 309 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock DataInfo;
#line default
#line hidden
#line 310 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.CheckBox RemoveDataBox;
#line default
#line hidden
#line 312 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock RemoveDataWarning;
#line default
#line hidden
#line 322 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Button BackButton;
#line default
#line hidden
#line 323 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Button NextButton;
#line default
#line hidden
#line 324 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Button CancelButton;
#line default
#line hidden
#line 328 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Grid DialogOverlay;
#line default
#line hidden
#line 329 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Border DialogCard;
#line default
#line hidden
#line 332 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Media.ScaleTransform DialogScale;
#line default
#line hidden
#line 336 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock DialogIcon;
#line default
#line hidden
#line 339 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock DialogTitle;
#line default
#line hidden
#line 342 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBlock DialogText;
#line default
#line hidden
#line 345 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Button DialogSecondary;
#line default
#line hidden
#line 346 "..\..\..\MainWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Button DialogPrimary;
#line default
#line hidden
private bool _contentLoaded;
/// <summary>
/// InitializeComponent
/// </summary>
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.13.0")]
public void InitializeComponent() {
if (_contentLoaded) {
return;
}
_contentLoaded = true;
System.Uri resourceLocater = new System.Uri("/Git-Manager-GUI-Setup;component/mainwindow.xaml", System.UriKind.Relative);
#line 1 "..\..\..\MainWindow.xaml"
System.Windows.Application.LoadComponent(this, resourceLocater);
#line default
#line hidden
}
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.13.0")]
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
void System.Windows.Markup.IComponentConnector.Connect(int connectionId, object target) {
switch (connectionId)
{
case 1:
this.VersionText = ((System.Windows.Controls.TextBlock)(target));
return;
case 2:
this.Steps = ((System.Windows.Controls.StackPanel)(target));
return;
case 3:
this.Dot0 = ((System.Windows.Controls.Border)(target));
return;
case 4:
this.Num0 = ((System.Windows.Controls.TextBlock)(target));
return;
case 5:
this.Label0 = ((System.Windows.Controls.TextBlock)(target));
return;
case 6:
this.Line0 = ((System.Windows.Controls.Border)(target));
return;
case 7:
this.Dot1 = ((System.Windows.Controls.Border)(target));
return;
case 8:
this.Num1 = ((System.Windows.Controls.TextBlock)(target));
return;
case 9:
this.Label1 = ((System.Windows.Controls.TextBlock)(target));
return;
case 10:
this.Line1 = ((System.Windows.Controls.Border)(target));
return;
case 11:
this.Dot2 = ((System.Windows.Controls.Border)(target));
return;
case 12:
this.Num2 = ((System.Windows.Controls.TextBlock)(target));
return;
case 13:
this.Label2 = ((System.Windows.Controls.TextBlock)(target));
return;
case 14:
this.WindowTitle = ((System.Windows.Controls.TextBlock)(target));
return;
case 15:
#line 172 "..\..\..\MainWindow.xaml"
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.Minimize_Click);
#line default
#line hidden
return;
case 16:
this.CloseButton = ((System.Windows.Controls.Button)(target));
#line 174 "..\..\..\MainWindow.xaml"
this.CloseButton.Click += new System.Windows.RoutedEventHandler(this.Cancel_Click);
#line default
#line hidden
return;
case 17:
this.PageWelcome = ((System.Windows.Controls.StackPanel)(target));
return;
case 18:
this.UpdateHint = ((System.Windows.Controls.Border)(target));
return;
case 19:
this.UpdateHintText = ((System.Windows.Controls.TextBlock)(target));
return;
case 20:
this.RunningHint = ((System.Windows.Controls.Border)(target));
return;
case 21:
this.PageOptions = ((System.Windows.Controls.StackPanel)(target));
return;
case 22:
#line 257 "..\..\..\MainWindow.xaml"
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.ChangeDir_Click);
#line default
#line hidden
return;
case 23:
this.DirBox = ((System.Windows.Controls.TextBox)(target));
return;
case 24:
this.DesktopLinkBox = ((System.Windows.Controls.CheckBox)(target));
return;
case 25:
this.PageProgress = ((System.Windows.Controls.StackPanel)(target));
return;
case 26:
this.SuccessIcon = ((System.Windows.Controls.Border)(target));
return;
case 27:
this.SuccessScale = ((System.Windows.Media.ScaleTransform)(target));
return;
case 28:
this.ProgressTitle = ((System.Windows.Controls.TextBlock)(target));
return;
case 29:
this.ProgressBarPanel = ((System.Windows.Controls.StackPanel)(target));
return;
case 30:
this.PercentText = ((System.Windows.Controls.TextBlock)(target));
return;
case 31:
this.ProgressTrack = ((System.Windows.Controls.Border)(target));
#line 286 "..\..\..\MainWindow.xaml"
this.ProgressTrack.SizeChanged += new System.Windows.SizeChangedEventHandler(this.ProgressTrack_SizeChanged);
#line default
#line hidden
return;
case 32:
this.ProgressFill = ((System.Windows.Controls.Border)(target));
return;
case 33:
this.ProgressText = ((System.Windows.Controls.TextBlock)(target));
return;
case 34:
this.DonePanel = ((System.Windows.Controls.StackPanel)(target));
return;
case 35:
this.LaunchBox = ((System.Windows.Controls.CheckBox)(target));
return;
case 36:
this.PageUninstall = ((System.Windows.Controls.StackPanel)(target));
return;
case 37:
this.DataInfo = ((System.Windows.Controls.TextBlock)(target));
return;
case 38:
this.RemoveDataBox = ((System.Windows.Controls.CheckBox)(target));
#line 311 "..\..\..\MainWindow.xaml"
this.RemoveDataBox.Checked += new System.Windows.RoutedEventHandler(this.RemoveDataBox_Click);
#line default
#line hidden
#line 311 "..\..\..\MainWindow.xaml"
this.RemoveDataBox.Unchecked += new System.Windows.RoutedEventHandler(this.RemoveDataBox_Click);
#line default
#line hidden
return;
case 39:
this.RemoveDataWarning = ((System.Windows.Controls.TextBlock)(target));
return;
case 40:
this.BackButton = ((System.Windows.Controls.Button)(target));
#line 322 "..\..\..\MainWindow.xaml"
this.BackButton.Click += new System.Windows.RoutedEventHandler(this.Back_Click);
#line default
#line hidden
return;
case 41:
this.NextButton = ((System.Windows.Controls.Button)(target));
#line 323 "..\..\..\MainWindow.xaml"
this.NextButton.Click += new System.Windows.RoutedEventHandler(this.Next_Click);
#line default
#line hidden
return;
case 42:
this.CancelButton = ((System.Windows.Controls.Button)(target));
#line 324 "..\..\..\MainWindow.xaml"
this.CancelButton.Click += new System.Windows.RoutedEventHandler(this.Cancel_Click);
#line default
#line hidden
return;
case 43:
this.DialogOverlay = ((System.Windows.Controls.Grid)(target));
return;
case 44:
this.DialogCard = ((System.Windows.Controls.Border)(target));
return;
case 45:
this.DialogScale = ((System.Windows.Media.ScaleTransform)(target));
return;
case 46:
this.DialogIcon = ((System.Windows.Controls.TextBlock)(target));
return;
case 47:
this.DialogTitle = ((System.Windows.Controls.TextBlock)(target));
return;
case 48:
this.DialogText = ((System.Windows.Controls.TextBlock)(target));
return;
case 49:
this.DialogSecondary = ((System.Windows.Controls.Button)(target));
#line 345 "..\..\..\MainWindow.xaml"
this.DialogSecondary.Click += new System.Windows.RoutedEventHandler(this.DialogSecondary_Click);
#line default
#line hidden
return;
case 50:
this.DialogPrimary = ((System.Windows.Controls.Button)(target));
#line 346 "..\..\..\MainWindow.xaml"
this.DialogPrimary.Click += new System.Windows.RoutedEventHandler(this.DialogPrimary_Click);
#line default
#line hidden
return;
}
this._contentLoaded = true;
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
+471
View File
@@ -0,0 +1,471 @@
{
"version": 3,
"targets": {
".NETFramework,Version=v4.8": {
"Microsoft.NETFramework.ReferenceAssemblies/1.0.3": {
"type": "package",
"dependencies": {
"Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3"
}
},
"Microsoft.NETFramework.ReferenceAssemblies.net48/1.0.3": {
"type": "package",
"build": {
"build/Microsoft.NETFramework.ReferenceAssemblies.net48.targets": {}
}
}
}
},
"libraries": {
"Microsoft.NETFramework.ReferenceAssemblies/1.0.3": {
"sha512": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
"type": "package",
"path": "microsoft.netframework.referenceassemblies/1.0.3",
"files": [
".nupkg.metadata",
".signature.p7s",
"microsoft.netframework.referenceassemblies.1.0.3.nupkg.sha512",
"microsoft.netframework.referenceassemblies.nuspec"
]
},
"Microsoft.NETFramework.ReferenceAssemblies.net48/1.0.3": {
"sha512": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ==",
"type": "package",
"path": "microsoft.netframework.referenceassemblies.net48/1.0.3",
"files": [
".nupkg.metadata",
".signature.p7s",
"build/.NETFramework/v4.8/Accessibility.dll",
"build/.NETFramework/v4.8/Accessibility.xml",
"build/.NETFramework/v4.8/CustomMarshalers.dll",
"build/.NETFramework/v4.8/CustomMarshalers.xml",
"build/.NETFramework/v4.8/Facades/Microsoft.Win32.Primitives.dll",
"build/.NETFramework/v4.8/Facades/System.AppContext.dll",
"build/.NETFramework/v4.8/Facades/System.Collections.Concurrent.dll",
"build/.NETFramework/v4.8/Facades/System.Collections.NonGeneric.dll",
"build/.NETFramework/v4.8/Facades/System.Collections.Specialized.dll",
"build/.NETFramework/v4.8/Facades/System.Collections.dll",
"build/.NETFramework/v4.8/Facades/System.ComponentModel.Annotations.dll",
"build/.NETFramework/v4.8/Facades/System.ComponentModel.EventBasedAsync.dll",
"build/.NETFramework/v4.8/Facades/System.ComponentModel.Primitives.dll",
"build/.NETFramework/v4.8/Facades/System.ComponentModel.TypeConverter.dll",
"build/.NETFramework/v4.8/Facades/System.ComponentModel.dll",
"build/.NETFramework/v4.8/Facades/System.Console.dll",
"build/.NETFramework/v4.8/Facades/System.Data.Common.dll",
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Contracts.dll",
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Debug.dll",
"build/.NETFramework/v4.8/Facades/System.Diagnostics.FileVersionInfo.dll",
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Process.dll",
"build/.NETFramework/v4.8/Facades/System.Diagnostics.StackTrace.dll",
"build/.NETFramework/v4.8/Facades/System.Diagnostics.TextWriterTraceListener.dll",
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Tools.dll",
"build/.NETFramework/v4.8/Facades/System.Diagnostics.TraceSource.dll",
"build/.NETFramework/v4.8/Facades/System.Drawing.Primitives.dll",
"build/.NETFramework/v4.8/Facades/System.Dynamic.Runtime.dll",
"build/.NETFramework/v4.8/Facades/System.Globalization.Calendars.dll",
"build/.NETFramework/v4.8/Facades/System.Globalization.Extensions.dll",
"build/.NETFramework/v4.8/Facades/System.Globalization.dll",
"build/.NETFramework/v4.8/Facades/System.IO.Compression.ZipFile.dll",
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.DriveInfo.dll",
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.Primitives.dll",
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.Watcher.dll",
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.dll",
"build/.NETFramework/v4.8/Facades/System.IO.IsolatedStorage.dll",
"build/.NETFramework/v4.8/Facades/System.IO.MemoryMappedFiles.dll",
"build/.NETFramework/v4.8/Facades/System.IO.Pipes.dll",
"build/.NETFramework/v4.8/Facades/System.IO.UnmanagedMemoryStream.dll",
"build/.NETFramework/v4.8/Facades/System.IO.dll",
"build/.NETFramework/v4.8/Facades/System.Linq.Expressions.dll",
"build/.NETFramework/v4.8/Facades/System.Linq.Parallel.dll",
"build/.NETFramework/v4.8/Facades/System.Linq.Queryable.dll",
"build/.NETFramework/v4.8/Facades/System.Linq.dll",
"build/.NETFramework/v4.8/Facades/System.Net.Http.Rtc.dll",
"build/.NETFramework/v4.8/Facades/System.Net.NameResolution.dll",
"build/.NETFramework/v4.8/Facades/System.Net.NetworkInformation.dll",
"build/.NETFramework/v4.8/Facades/System.Net.Ping.dll",
"build/.NETFramework/v4.8/Facades/System.Net.Primitives.dll",
"build/.NETFramework/v4.8/Facades/System.Net.Requests.dll",
"build/.NETFramework/v4.8/Facades/System.Net.Security.dll",
"build/.NETFramework/v4.8/Facades/System.Net.Sockets.dll",
"build/.NETFramework/v4.8/Facades/System.Net.WebHeaderCollection.dll",
"build/.NETFramework/v4.8/Facades/System.Net.WebSockets.Client.dll",
"build/.NETFramework/v4.8/Facades/System.Net.WebSockets.dll",
"build/.NETFramework/v4.8/Facades/System.ObjectModel.dll",
"build/.NETFramework/v4.8/Facades/System.Reflection.Emit.ILGeneration.dll",
"build/.NETFramework/v4.8/Facades/System.Reflection.Emit.Lightweight.dll",
"build/.NETFramework/v4.8/Facades/System.Reflection.Emit.dll",
"build/.NETFramework/v4.8/Facades/System.Reflection.Extensions.dll",
"build/.NETFramework/v4.8/Facades/System.Reflection.Primitives.dll",
"build/.NETFramework/v4.8/Facades/System.Reflection.dll",
"build/.NETFramework/v4.8/Facades/System.Resources.Reader.dll",
"build/.NETFramework/v4.8/Facades/System.Resources.ResourceManager.dll",
"build/.NETFramework/v4.8/Facades/System.Resources.Writer.dll",
"build/.NETFramework/v4.8/Facades/System.Runtime.CompilerServices.VisualC.dll",
"build/.NETFramework/v4.8/Facades/System.Runtime.Extensions.dll",
"build/.NETFramework/v4.8/Facades/System.Runtime.Handles.dll",
"build/.NETFramework/v4.8/Facades/System.Runtime.InteropServices.RuntimeInformation.dll",
"build/.NETFramework/v4.8/Facades/System.Runtime.InteropServices.WindowsRuntime.dll",
"build/.NETFramework/v4.8/Facades/System.Runtime.InteropServices.dll",
"build/.NETFramework/v4.8/Facades/System.Runtime.Numerics.dll",
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Formatters.dll",
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Json.dll",
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Primitives.dll",
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Xml.dll",
"build/.NETFramework/v4.8/Facades/System.Runtime.dll",
"build/.NETFramework/v4.8/Facades/System.Security.Claims.dll",
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Algorithms.dll",
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Csp.dll",
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Encoding.dll",
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Primitives.dll",
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.X509Certificates.dll",
"build/.NETFramework/v4.8/Facades/System.Security.Principal.dll",
"build/.NETFramework/v4.8/Facades/System.Security.SecureString.dll",
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Duplex.dll",
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Http.dll",
"build/.NETFramework/v4.8/Facades/System.ServiceModel.NetTcp.dll",
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Primitives.dll",
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Security.dll",
"build/.NETFramework/v4.8/Facades/System.Text.Encoding.Extensions.dll",
"build/.NETFramework/v4.8/Facades/System.Text.Encoding.dll",
"build/.NETFramework/v4.8/Facades/System.Text.RegularExpressions.dll",
"build/.NETFramework/v4.8/Facades/System.Threading.Overlapped.dll",
"build/.NETFramework/v4.8/Facades/System.Threading.Tasks.Parallel.dll",
"build/.NETFramework/v4.8/Facades/System.Threading.Tasks.dll",
"build/.NETFramework/v4.8/Facades/System.Threading.Thread.dll",
"build/.NETFramework/v4.8/Facades/System.Threading.ThreadPool.dll",
"build/.NETFramework/v4.8/Facades/System.Threading.Timer.dll",
"build/.NETFramework/v4.8/Facades/System.Threading.dll",
"build/.NETFramework/v4.8/Facades/System.ValueTuple.dll",
"build/.NETFramework/v4.8/Facades/System.Xml.ReaderWriter.dll",
"build/.NETFramework/v4.8/Facades/System.Xml.XDocument.dll",
"build/.NETFramework/v4.8/Facades/System.Xml.XPath.XDocument.dll",
"build/.NETFramework/v4.8/Facades/System.Xml.XPath.dll",
"build/.NETFramework/v4.8/Facades/System.Xml.XmlDocument.dll",
"build/.NETFramework/v4.8/Facades/System.Xml.XmlSerializer.dll",
"build/.NETFramework/v4.8/Facades/netstandard.dll",
"build/.NETFramework/v4.8/ISymWrapper.dll",
"build/.NETFramework/v4.8/ISymWrapper.xml",
"build/.NETFramework/v4.8/Microsoft.Activities.Build.dll",
"build/.NETFramework/v4.8/Microsoft.Activities.Build.xml",
"build/.NETFramework/v4.8/Microsoft.Build.Conversion.v4.0.dll",
"build/.NETFramework/v4.8/Microsoft.Build.Conversion.v4.0.xml",
"build/.NETFramework/v4.8/Microsoft.Build.Engine.dll",
"build/.NETFramework/v4.8/Microsoft.Build.Engine.xml",
"build/.NETFramework/v4.8/Microsoft.Build.Framework.dll",
"build/.NETFramework/v4.8/Microsoft.Build.Framework.xml",
"build/.NETFramework/v4.8/Microsoft.Build.Tasks.v4.0.dll",
"build/.NETFramework/v4.8/Microsoft.Build.Tasks.v4.0.xml",
"build/.NETFramework/v4.8/Microsoft.Build.Utilities.v4.0.dll",
"build/.NETFramework/v4.8/Microsoft.Build.Utilities.v4.0.xml",
"build/.NETFramework/v4.8/Microsoft.Build.dll",
"build/.NETFramework/v4.8/Microsoft.Build.xml",
"build/.NETFramework/v4.8/Microsoft.CSharp.dll",
"build/.NETFramework/v4.8/Microsoft.CSharp.xml",
"build/.NETFramework/v4.8/Microsoft.JScript.dll",
"build/.NETFramework/v4.8/Microsoft.JScript.xml",
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.Data.dll",
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.Data.xml",
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.dll",
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.xml",
"build/.NETFramework/v4.8/Microsoft.VisualBasic.dll",
"build/.NETFramework/v4.8/Microsoft.VisualBasic.xml",
"build/.NETFramework/v4.8/Microsoft.VisualC.STLCLR.dll",
"build/.NETFramework/v4.8/Microsoft.VisualC.STLCLR.xml",
"build/.NETFramework/v4.8/Microsoft.VisualC.dll",
"build/.NETFramework/v4.8/Microsoft.VisualC.xml",
"build/.NETFramework/v4.8/PermissionSets/FullTrust.xml",
"build/.NETFramework/v4.8/PermissionSets/Internet.xml",
"build/.NETFramework/v4.8/PermissionSets/LocalIntranet.xml",
"build/.NETFramework/v4.8/PresentationBuildTasks.dll",
"build/.NETFramework/v4.8/PresentationBuildTasks.xml",
"build/.NETFramework/v4.8/PresentationCore.dll",
"build/.NETFramework/v4.8/PresentationCore.xml",
"build/.NETFramework/v4.8/PresentationFramework.Aero.dll",
"build/.NETFramework/v4.8/PresentationFramework.Aero.xml",
"build/.NETFramework/v4.8/PresentationFramework.Aero2.dll",
"build/.NETFramework/v4.8/PresentationFramework.Aero2.xml",
"build/.NETFramework/v4.8/PresentationFramework.AeroLite.dll",
"build/.NETFramework/v4.8/PresentationFramework.AeroLite.xml",
"build/.NETFramework/v4.8/PresentationFramework.Classic.dll",
"build/.NETFramework/v4.8/PresentationFramework.Classic.xml",
"build/.NETFramework/v4.8/PresentationFramework.Luna.dll",
"build/.NETFramework/v4.8/PresentationFramework.Luna.xml",
"build/.NETFramework/v4.8/PresentationFramework.Royale.dll",
"build/.NETFramework/v4.8/PresentationFramework.Royale.xml",
"build/.NETFramework/v4.8/PresentationFramework.dll",
"build/.NETFramework/v4.8/PresentationFramework.xml",
"build/.NETFramework/v4.8/ReachFramework.dll",
"build/.NETFramework/v4.8/ReachFramework.xml",
"build/.NETFramework/v4.8/RedistList/FrameworkList.xml",
"build/.NETFramework/v4.8/System.Activities.Core.Presentation.dll",
"build/.NETFramework/v4.8/System.Activities.Core.Presentation.xml",
"build/.NETFramework/v4.8/System.Activities.DurableInstancing.dll",
"build/.NETFramework/v4.8/System.Activities.DurableInstancing.xml",
"build/.NETFramework/v4.8/System.Activities.Presentation.dll",
"build/.NETFramework/v4.8/System.Activities.Presentation.xml",
"build/.NETFramework/v4.8/System.Activities.dll",
"build/.NETFramework/v4.8/System.Activities.xml",
"build/.NETFramework/v4.8/System.AddIn.Contract.dll",
"build/.NETFramework/v4.8/System.AddIn.Contract.xml",
"build/.NETFramework/v4.8/System.AddIn.dll",
"build/.NETFramework/v4.8/System.AddIn.xml",
"build/.NETFramework/v4.8/System.ComponentModel.Composition.Registration.dll",
"build/.NETFramework/v4.8/System.ComponentModel.Composition.Registration.xml",
"build/.NETFramework/v4.8/System.ComponentModel.Composition.dll",
"build/.NETFramework/v4.8/System.ComponentModel.Composition.xml",
"build/.NETFramework/v4.8/System.ComponentModel.DataAnnotations.dll",
"build/.NETFramework/v4.8/System.ComponentModel.DataAnnotations.xml",
"build/.NETFramework/v4.8/System.Configuration.Install.dll",
"build/.NETFramework/v4.8/System.Configuration.Install.xml",
"build/.NETFramework/v4.8/System.Configuration.dll",
"build/.NETFramework/v4.8/System.Configuration.xml",
"build/.NETFramework/v4.8/System.Core.dll",
"build/.NETFramework/v4.8/System.Core.xml",
"build/.NETFramework/v4.8/System.Data.DataSetExtensions.dll",
"build/.NETFramework/v4.8/System.Data.DataSetExtensions.xml",
"build/.NETFramework/v4.8/System.Data.Entity.Design.dll",
"build/.NETFramework/v4.8/System.Data.Entity.Design.xml",
"build/.NETFramework/v4.8/System.Data.Entity.dll",
"build/.NETFramework/v4.8/System.Data.Entity.xml",
"build/.NETFramework/v4.8/System.Data.Linq.dll",
"build/.NETFramework/v4.8/System.Data.Linq.xml",
"build/.NETFramework/v4.8/System.Data.OracleClient.dll",
"build/.NETFramework/v4.8/System.Data.OracleClient.xml",
"build/.NETFramework/v4.8/System.Data.Services.Client.dll",
"build/.NETFramework/v4.8/System.Data.Services.Client.xml",
"build/.NETFramework/v4.8/System.Data.Services.Design.dll",
"build/.NETFramework/v4.8/System.Data.Services.Design.xml",
"build/.NETFramework/v4.8/System.Data.Services.dll",
"build/.NETFramework/v4.8/System.Data.Services.xml",
"build/.NETFramework/v4.8/System.Data.SqlXml.dll",
"build/.NETFramework/v4.8/System.Data.SqlXml.xml",
"build/.NETFramework/v4.8/System.Data.dll",
"build/.NETFramework/v4.8/System.Data.xml",
"build/.NETFramework/v4.8/System.Deployment.dll",
"build/.NETFramework/v4.8/System.Deployment.xml",
"build/.NETFramework/v4.8/System.Design.dll",
"build/.NETFramework/v4.8/System.Design.xml",
"build/.NETFramework/v4.8/System.Device.dll",
"build/.NETFramework/v4.8/System.Device.xml",
"build/.NETFramework/v4.8/System.Diagnostics.Tracing.dll",
"build/.NETFramework/v4.8/System.Diagnostics.Tracing.xml",
"build/.NETFramework/v4.8/System.DirectoryServices.AccountManagement.dll",
"build/.NETFramework/v4.8/System.DirectoryServices.AccountManagement.xml",
"build/.NETFramework/v4.8/System.DirectoryServices.Protocols.dll",
"build/.NETFramework/v4.8/System.DirectoryServices.Protocols.xml",
"build/.NETFramework/v4.8/System.DirectoryServices.dll",
"build/.NETFramework/v4.8/System.DirectoryServices.xml",
"build/.NETFramework/v4.8/System.Drawing.Design.dll",
"build/.NETFramework/v4.8/System.Drawing.Design.xml",
"build/.NETFramework/v4.8/System.Drawing.dll",
"build/.NETFramework/v4.8/System.Drawing.xml",
"build/.NETFramework/v4.8/System.Dynamic.dll",
"build/.NETFramework/v4.8/System.EnterpriseServices.Thunk.dll",
"build/.NETFramework/v4.8/System.EnterpriseServices.Wrapper.dll",
"build/.NETFramework/v4.8/System.EnterpriseServices.dll",
"build/.NETFramework/v4.8/System.EnterpriseServices.xml",
"build/.NETFramework/v4.8/System.IO.Compression.FileSystem.dll",
"build/.NETFramework/v4.8/System.IO.Compression.FileSystem.xml",
"build/.NETFramework/v4.8/System.IO.Compression.dll",
"build/.NETFramework/v4.8/System.IO.Compression.xml",
"build/.NETFramework/v4.8/System.IO.Log.dll",
"build/.NETFramework/v4.8/System.IO.Log.xml",
"build/.NETFramework/v4.8/System.IdentityModel.Selectors.dll",
"build/.NETFramework/v4.8/System.IdentityModel.Selectors.xml",
"build/.NETFramework/v4.8/System.IdentityModel.Services.dll",
"build/.NETFramework/v4.8/System.IdentityModel.Services.xml",
"build/.NETFramework/v4.8/System.IdentityModel.dll",
"build/.NETFramework/v4.8/System.IdentityModel.xml",
"build/.NETFramework/v4.8/System.Linq.xml",
"build/.NETFramework/v4.8/System.Management.Instrumentation.dll",
"build/.NETFramework/v4.8/System.Management.Instrumentation.xml",
"build/.NETFramework/v4.8/System.Management.dll",
"build/.NETFramework/v4.8/System.Management.xml",
"build/.NETFramework/v4.8/System.Messaging.dll",
"build/.NETFramework/v4.8/System.Messaging.xml",
"build/.NETFramework/v4.8/System.Net.Http.WebRequest.dll",
"build/.NETFramework/v4.8/System.Net.Http.WebRequest.xml",
"build/.NETFramework/v4.8/System.Net.Http.dll",
"build/.NETFramework/v4.8/System.Net.Http.xml",
"build/.NETFramework/v4.8/System.Net.dll",
"build/.NETFramework/v4.8/System.Net.xml",
"build/.NETFramework/v4.8/System.Numerics.dll",
"build/.NETFramework/v4.8/System.Numerics.xml",
"build/.NETFramework/v4.8/System.Printing.dll",
"build/.NETFramework/v4.8/System.Printing.xml",
"build/.NETFramework/v4.8/System.Reflection.Context.dll",
"build/.NETFramework/v4.8/System.Reflection.Context.xml",
"build/.NETFramework/v4.8/System.Runtime.Caching.dll",
"build/.NETFramework/v4.8/System.Runtime.Caching.xml",
"build/.NETFramework/v4.8/System.Runtime.DurableInstancing.dll",
"build/.NETFramework/v4.8/System.Runtime.DurableInstancing.xml",
"build/.NETFramework/v4.8/System.Runtime.Remoting.dll",
"build/.NETFramework/v4.8/System.Runtime.Remoting.xml",
"build/.NETFramework/v4.8/System.Runtime.Serialization.Formatters.Soap.dll",
"build/.NETFramework/v4.8/System.Runtime.Serialization.Formatters.Soap.xml",
"build/.NETFramework/v4.8/System.Runtime.Serialization.dll",
"build/.NETFramework/v4.8/System.Runtime.Serialization.xml",
"build/.NETFramework/v4.8/System.Security.dll",
"build/.NETFramework/v4.8/System.Security.xml",
"build/.NETFramework/v4.8/System.ServiceModel.Activation.dll",
"build/.NETFramework/v4.8/System.ServiceModel.Activation.xml",
"build/.NETFramework/v4.8/System.ServiceModel.Activities.dll",
"build/.NETFramework/v4.8/System.ServiceModel.Activities.xml",
"build/.NETFramework/v4.8/System.ServiceModel.Channels.dll",
"build/.NETFramework/v4.8/System.ServiceModel.Channels.xml",
"build/.NETFramework/v4.8/System.ServiceModel.Discovery.dll",
"build/.NETFramework/v4.8/System.ServiceModel.Discovery.xml",
"build/.NETFramework/v4.8/System.ServiceModel.Routing.dll",
"build/.NETFramework/v4.8/System.ServiceModel.Routing.xml",
"build/.NETFramework/v4.8/System.ServiceModel.Web.dll",
"build/.NETFramework/v4.8/System.ServiceModel.Web.xml",
"build/.NETFramework/v4.8/System.ServiceModel.dll",
"build/.NETFramework/v4.8/System.ServiceModel.xml",
"build/.NETFramework/v4.8/System.ServiceProcess.dll",
"build/.NETFramework/v4.8/System.ServiceProcess.xml",
"build/.NETFramework/v4.8/System.Speech.dll",
"build/.NETFramework/v4.8/System.Speech.xml",
"build/.NETFramework/v4.8/System.Threading.Tasks.Dataflow.xml",
"build/.NETFramework/v4.8/System.Transactions.dll",
"build/.NETFramework/v4.8/System.Transactions.xml",
"build/.NETFramework/v4.8/System.Web.Abstractions.dll",
"build/.NETFramework/v4.8/System.Web.ApplicationServices.dll",
"build/.NETFramework/v4.8/System.Web.ApplicationServices.xml",
"build/.NETFramework/v4.8/System.Web.DataVisualization.Design.dll",
"build/.NETFramework/v4.8/System.Web.DataVisualization.dll",
"build/.NETFramework/v4.8/System.Web.DataVisualization.xml",
"build/.NETFramework/v4.8/System.Web.DynamicData.Design.dll",
"build/.NETFramework/v4.8/System.Web.DynamicData.Design.xml",
"build/.NETFramework/v4.8/System.Web.DynamicData.dll",
"build/.NETFramework/v4.8/System.Web.DynamicData.xml",
"build/.NETFramework/v4.8/System.Web.Entity.Design.dll",
"build/.NETFramework/v4.8/System.Web.Entity.Design.xml",
"build/.NETFramework/v4.8/System.Web.Entity.dll",
"build/.NETFramework/v4.8/System.Web.Entity.xml",
"build/.NETFramework/v4.8/System.Web.Extensions.Design.dll",
"build/.NETFramework/v4.8/System.Web.Extensions.Design.xml",
"build/.NETFramework/v4.8/System.Web.Extensions.dll",
"build/.NETFramework/v4.8/System.Web.Extensions.xml",
"build/.NETFramework/v4.8/System.Web.Mobile.dll",
"build/.NETFramework/v4.8/System.Web.Mobile.xml",
"build/.NETFramework/v4.8/System.Web.RegularExpressions.dll",
"build/.NETFramework/v4.8/System.Web.RegularExpressions.xml",
"build/.NETFramework/v4.8/System.Web.Routing.dll",
"build/.NETFramework/v4.8/System.Web.Services.dll",
"build/.NETFramework/v4.8/System.Web.Services.xml",
"build/.NETFramework/v4.8/System.Web.dll",
"build/.NETFramework/v4.8/System.Web.xml",
"build/.NETFramework/v4.8/System.Windows.Controls.Ribbon.dll",
"build/.NETFramework/v4.8/System.Windows.Controls.Ribbon.xml",
"build/.NETFramework/v4.8/System.Windows.Forms.DataVisualization.Design.dll",
"build/.NETFramework/v4.8/System.Windows.Forms.DataVisualization.dll",
"build/.NETFramework/v4.8/System.Windows.Forms.DataVisualization.xml",
"build/.NETFramework/v4.8/System.Windows.Forms.dll",
"build/.NETFramework/v4.8/System.Windows.Forms.xml",
"build/.NETFramework/v4.8/System.Windows.Input.Manipulations.dll",
"build/.NETFramework/v4.8/System.Windows.Input.Manipulations.xml",
"build/.NETFramework/v4.8/System.Windows.Presentation.dll",
"build/.NETFramework/v4.8/System.Windows.Presentation.xml",
"build/.NETFramework/v4.8/System.Windows.dll",
"build/.NETFramework/v4.8/System.Workflow.Activities.dll",
"build/.NETFramework/v4.8/System.Workflow.Activities.xml",
"build/.NETFramework/v4.8/System.Workflow.ComponentModel.dll",
"build/.NETFramework/v4.8/System.Workflow.ComponentModel.xml",
"build/.NETFramework/v4.8/System.Workflow.Runtime.dll",
"build/.NETFramework/v4.8/System.Workflow.Runtime.xml",
"build/.NETFramework/v4.8/System.WorkflowServices.dll",
"build/.NETFramework/v4.8/System.WorkflowServices.xml",
"build/.NETFramework/v4.8/System.Xaml.dll",
"build/.NETFramework/v4.8/System.Xaml.xml",
"build/.NETFramework/v4.8/System.Xml.Linq.dll",
"build/.NETFramework/v4.8/System.Xml.Linq.xml",
"build/.NETFramework/v4.8/System.Xml.Serialization.dll",
"build/.NETFramework/v4.8/System.Xml.dll",
"build/.NETFramework/v4.8/System.Xml.xml",
"build/.NETFramework/v4.8/System.dll",
"build/.NETFramework/v4.8/System.xml",
"build/.NETFramework/v4.8/UIAutomationClient.dll",
"build/.NETFramework/v4.8/UIAutomationClient.xml",
"build/.NETFramework/v4.8/UIAutomationClientsideProviders.dll",
"build/.NETFramework/v4.8/UIAutomationClientsideProviders.xml",
"build/.NETFramework/v4.8/UIAutomationProvider.dll",
"build/.NETFramework/v4.8/UIAutomationProvider.xml",
"build/.NETFramework/v4.8/UIAutomationTypes.dll",
"build/.NETFramework/v4.8/UIAutomationTypes.xml",
"build/.NETFramework/v4.8/WindowsBase.dll",
"build/.NETFramework/v4.8/WindowsBase.xml",
"build/.NETFramework/v4.8/WindowsFormsIntegration.dll",
"build/.NETFramework/v4.8/WindowsFormsIntegration.xml",
"build/.NETFramework/v4.8/XamlBuildTask.dll",
"build/.NETFramework/v4.8/XamlBuildTask.xml",
"build/.NETFramework/v4.8/mscorlib.dll",
"build/.NETFramework/v4.8/mscorlib.xml",
"build/.NETFramework/v4.8/namespaces.xml",
"build/.NETFramework/v4.8/sysglobl.dll",
"build/.NETFramework/v4.8/sysglobl.xml",
"build/Microsoft.NETFramework.ReferenceAssemblies.net48.targets",
"microsoft.netframework.referenceassemblies.net48.1.0.3.nupkg.sha512",
"microsoft.netframework.referenceassemblies.net48.nuspec"
]
}
},
"projectFileDependencyGroups": {
".NETFramework,Version=v4.8": [
"Microsoft.NETFramework.ReferenceAssemblies >= 1.0.3"
]
},
"packageFolders": {
"C:\\Users\\Viper\\.nuget\\packages\\": {}
},
"project": {
"version": "0.0.0",
"restore": {
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\git-manager-gui\\installer\\GitManagerSetup.csproj",
"projectName": "Git-Manager-GUI-Setup",
"projectPath": "C:\\Users\\Viper\\Desktop\\git-manager-gui\\installer\\GitManagerSetup.csproj",
"packagesPath": "C:\\Users\\Viper\\.nuget\\packages\\",
"outputPath": "C:\\Users\\Viper\\Desktop\\git-manager-gui\\installer\\obj\\",
"projectStyle": "PackageReference",
"configFilePaths": [
"C:\\Users\\Viper\\AppData\\Roaming\\NuGet\\NuGet.Config"
],
"originalTargetFrameworks": [
"net48"
],
"sources": {
"https://api.nuget.org/v3/index.json": {}
},
"frameworks": {
"net48": {
"targetAlias": "net48",
"projectReferences": {}
}
},
"warningProperties": {
"warnAsError": [
"NU1605"
]
},
"restoreAuditProperties": {
"enableAudit": "true",
"auditLevel": "low",
"auditMode": "direct"
},
"SdkAnalysisLevel": "10.0.100"
},
"frameworks": {
"net48": {
"targetAlias": "net48",
"dependencies": {
"Microsoft.NETFramework.ReferenceAssemblies": {
"suppressParent": "All",
"target": "Package",
"version": "[1.0.3, )",
"autoReferenced": true
}
},
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.113\\RuntimeIdentifierGraph.json"
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"version": 2,
"dgSpecHash": "5on68f3wDDM=",
"success": true,
"projectFilePath": "C:\\Users\\Viper\\Desktop\\git-manager-gui\\installer\\GitManagerSetup.csproj",
"expectedPackageFiles": [
"C:\\Users\\Viper\\.nuget\\packages\\microsoft.netframework.referenceassemblies\\1.0.3\\microsoft.netframework.referenceassemblies.1.0.3.nupkg.sha512",
"C:\\Users\\Viper\\.nuget\\packages\\microsoft.netframework.referenceassemblies.net48\\1.0.3\\microsoft.netframework.referenceassemblies.net48.1.0.3.nupkg.sha512"
],
"logs": []
}
+7 -7
View File
@@ -490,13 +490,13 @@
<!-- Features -->
<section class="section" id="features">
<h2 class="section-title">Warum Git Manager Pro?</h2>
<p class="section-desc">Alles was du brauchst um deine Repositories effizient zu verwalten.</p>
<p class="section-desc">Alles, was du brauchst, um deine Repositories effizient zu verwalten.</p>
<div class="features-grid">
<div class="feature-card">
<div class="feature-icon">🌐</div>
<h3>Gitea & GitHub</h3>
<p>Unterstützt beide Plattformen seamless — alle Repositories an einem Ort.</p>
<p>Unterstützt beide Plattformen nahtlos — alle Repositories an einem Ort.</p>
</div>
<div class="feature-card">
<div class="feature-icon">📂</div>
@@ -535,16 +535,16 @@
</div>
<div class="feature-card">
<div class="feature-icon">📥</div>
<h3>Ein-Klick Migration</h3>
<h3>Ein-Klick-Migration</h3>
<p>Migriere Repos zwischen Services — mit kompletter History.</p>
</div>
</div>
<div class="demo-section">
<h3 style="color: var(--accent2); margin-bottom: 16px;">🎥 Demo (baldiger Update)</h3>
<h3 style="color: var(--accent2); margin-bottom: 16px;">🎥 Demo (baldiges Update)</h3>
<div class="demo-mockup">
Screenshot-Bereich — Demo und UI-Preview kommen bald!<br><br>
<em>Starte die App mit npm start um einen Live-Demo zu sehen</em>
Screenshot-Bereich — Demo und UI-Vorschau kommen bald!<br><br>
<em>Starte die App mit npm start, um eine Live-Demo zu sehen</em>
</div>
</div>
</section>
@@ -603,7 +603,7 @@
<div class="links-section">
<div class="link-card">
<h3>📖 Benutzerhandbuch</h3>
<p>Vollständiges Handbuch mit Screenshots und Schritt-für-Schritt Anleitungen.</p>
<p>Vollständiges Handbuch mit Screenshots und Schritt-für-Schritt-Anleitungen.</p>
<a href="HANDBUCH.html" target="_blank">Zum Handbuch →</a>
</div>
<div class="link-card">

Some files were not shown because too many files have changed in this diff Show More